Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/api/analytics_api.js19
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js60
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js29
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js38
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js20
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js58
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue60
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js36
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/utils.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue10
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb4
-rw-r--r--app/graphql/types/user_callout_type.rb2
-rw-r--r--app/models/wiki.rb11
-rw-r--r--app/services/wikis/create_attachment_service.rb4
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml2
-rw-r--r--config/feature_flags/development/trace_memory_allocations.yml8
-rw-r--r--doc/administration/lfs/index.md23
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/development/fe_guide/frontend_faq.md2
-rw-r--r--doc/development/multi_version_compatibility.md24
-rw-r--r--doc/user/clusters/agent/ci_cd_tunnel.md4
-rw-r--r--doc/user/clusters/integrations.md4
-rw-r--r--doc/user/project/import/fogbugz.md37
-rw-r--r--lib/api/ci/runners.rb2
-rw-r--r--lib/gitlab/memory/instrumentation.rb17
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb6
-rw-r--r--package.json2
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb2
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js9
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js88
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js69
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js10
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap2
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb17
-rw-r--r--spec/lib/gitlab/memory/instrumentation_spec.rb52
-rw-r--r--spec/services/event_create_service_spec.rb6
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb19
-rw-r--r--yarn.lock8
47 files changed, 599 insertions, 248 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 235d02efa49..38d59af5aed 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -2,7 +2,7 @@ review-cleanup:
extends:
- .default-retry
- .review:rules:review-cleanup
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3.5-kubectl1.17
stage: prepare
environment:
name: review/auto-cleanup
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7cd6346ac01..76ae4aa70a8 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-ebabe4399c781ea1f4a1a43774b14489446f1f68
+b56e0680f4d85505205945c39c01aa641d37bd9b
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js
index 58494c5a2b8..fd9b0160b0d 100644
--- a/app/assets/javascripts/api/analytics_api.js
+++ b/app/assets/javascripts/api/analytics_api.js
@@ -1,6 +1,8 @@
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
+const GROUP_VSA_PATH_BASE =
+ '/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
@@ -13,6 +15,12 @@ const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
};
+const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) =>
+ buildApiUrl(GROUP_VSA_PATH_BASE)
+ .replace(':id', groupId)
+ .replace(':value_stream_id', valueStreamId)
+ .replace(':stage_id', stageId);
+
export const getProjectValueStreams = (projectPath) => {
const url = buildProjectValueStreamPath(projectPath);
return axios.get(url);
@@ -30,3 +38,14 @@ export const getProjectValueStreamStageData = ({ requestPath, stageId, params })
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
+
+/**
+ * Shared group VSA paths
+ * We share some endpoints across and group and project level VSA
+ * When used for project level VSA, requests should include the `project_id` in the params object
+ */
+
+export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => {
+ const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId });
+ return axios.get(`${stageBase}/median`, { params });
+};
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 57cb220d9c9..615f96c3860 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -8,11 +8,24 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
+ const {
+ noAccessSvgPath,
+ noDataSvgPath,
+ requestPath,
+ fullPath,
+ projectId,
+ groupPath,
+ } = el.dataset;
store.dispatch('initializeVsa', {
+ projectId: parseInt(projectId, 10),
+ groupPath,
requestPath,
fullPath,
+ features: {
+ cycleAnalyticsForGroups:
+ (groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
+ },
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index ab852cbbb2d..955f0c7271e 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -3,6 +3,7 @@ import {
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
+ getValueStreamStageMedian,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
};
export const fetchValueStreams = ({ commit, dispatch, state }) => {
- const { fullPath } = state;
+ const {
+ fullPath,
+ features: { cycleAnalyticsForGroups },
+ } = state;
commit(types.REQUEST_VALUE_STREAMS);
+ const stageRequests = ['setSelectedStage'];
+ if (cycleAnalyticsForGroups) {
+ stageRequests.push('fetchStageMedians');
+ }
+
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => dispatch('setSelectedStage'))
+ .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
-export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
+export const fetchCycleAnalyticsData = ({
+ state: { requestPath },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
- return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
+ return getProjectValueStreamMetrics(requestPath, legacyFilterParams)
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
@@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
});
};
-export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
+export const fetchStageData = ({
+ state: { requestPath, selectedStage },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_STAGE_DATA);
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
- params: { 'cycle_analytics[start_date]': startDate },
+ params: legacyFilterParams,
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
@@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};
+const getStageMedians = ({ stageId, vsaParams, filterParams = {} }) => {
+ return getValueStreamStageMedian({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
+ id: stageId,
+ value: data?.value || null,
+ }));
+};
+
+export const fetchStageMedians = ({
+ state: { stages },
+ getters: { requestParams: vsaParams, filterParams },
+ commit,
+}) => {
+ commit(types.REQUEST_STAGE_MEDIANS);
+ return Promise.all(
+ stages.map(({ id: stageId }) =>
+ getStageMedians({
+ vsaParams,
+ stageId,
+ filterParams,
+ }),
+ ),
+ )
+ .then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
+ .catch((error) => {
+ commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
+ createFlash({
+ message: __('There was an error fetching median data for stages'),
+ });
+ });
+};
+
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
index c60a70ef147..66971ea8a2e 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -1,3 +1,5 @@
+import dateFormat from 'dateformat';
+import { dateFormats } from '~/analytics/shared/constants';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
@@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
selectedStage,
});
};
+
+export const requestParams = (state) => {
+ const {
+ selectedStage: { id: stageId = null },
+ groupPath: groupId,
+ selectedValueStream: { id: valueStreamId },
+ } = state;
+ return { valueStreamId, groupId, stageId };
+};
+
+const dateRangeParams = ({ createdAfter, createdBefore }) => ({
+ created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
+ created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
+});
+
+export const legacyFilterParams = ({ startDate }) => {
+ return {
+ 'cycle_analytics[start_date]': startDate,
+ };
+};
+
+export const filterParams = ({ id, ...rest }) => {
+ return {
+ project_ids: [id],
+ ...dateRangeParams(rest),
+ };
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
index 4f3d430ec9f..11ed62a4081 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
@@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_
export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
+
+export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
+export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
+export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index 0ae80116cd2..a8b7a607b66 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,11 +1,23 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { decorateData, decorateEvents, formatMedianValues } from '../utils';
+import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
+import {
+ decorateData,
+ decorateEvents,
+ formatMedianValues,
+ calculateFormattedDayInPast,
+} from '../utils';
import * as types from './mutation_types';
export default {
- [types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
+ [types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) {
state.requestPath = requestPath;
state.fullPath = fullPath;
+ state.groupPath = groupPath;
+ state.id = projectId;
+ const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ state.createdBefore = now;
+ state.createdAfter = past;
+ state.features = features;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
@@ -18,6 +30,9 @@ export default {
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
+ const { now, past } = calculateFormattedDayInPast(startDate);
+ state.createdBefore = now;
+ state.createdAfter = past;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
@@ -46,17 +61,25 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.hasError = false;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
const { summary, medians } = decorateData(data);
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = formatMedianValues(medians);
+ }
state.permissions = data.permissions;
state.summary = summary;
- state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.hasError = true;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true;
@@ -78,4 +101,13 @@ export default {
state.hasError = true;
state.selectedStageError = error;
},
+ [types.REQUEST_STAGE_MEDIANS](state) {
+ state.medians = {};
+ },
+ [types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians) {
+ state.medians = formatMedianValues(medians);
+ },
+ [types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
+ state.medians = {};
+ },
};
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
index 02f953d9517..4d61077fb99 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -1,9 +1,13 @@
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
+ features: {},
+ id: null,
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
+ createdAfter: null,
+ createdBefore: null,
stages: [],
summary: [],
analytics: [],
@@ -19,4 +23,5 @@ export default () => ({
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
+ parentPath: null,
});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 40ad7d8b2fc..a1690dd1513 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,6 +1,9 @@
+import dateFormat from 'dateformat';
import { unescape } from 'lodash';
+import { dateFormats } from '~/analytics/shared/constants';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
@@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
+
+const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
+
+/**
+ * Takes an integer specifying the number of days to subtract
+ * from the date specified will return the 2 dates, formatted as ISO dates
+ *
+ * @param {Number} daysInPast - Number of days in the past to subtract
+ * @param {Date} [today=new Date] - Date to subtract days from, defaults to today
+ * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
+ */
+export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
+ return {
+ now: toIsoFormat(today),
+ past: toIsoFormat(getDateInPast(today, daysInPast)),
+ };
+};
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index eaf396a7a59..5ee00464a8b 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => {
export function insertFinalNewline(content, endOfLine = '\n') {
return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content;
}
+
+export const markdownConfig = {
+ // allowedTags from GitLab's inline HTML guidelines
+ // https://docs.gitlab.com/ee/user/markdown.html#inline-html
+ ALLOWED_TAGS: [
+ 'a',
+ 'abbr',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'dd',
+ 'del',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'hr',
+ 'i',
+ 'img',
+ 'ins',
+ 'kbd',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
+ 'q',
+ 'rp',
+ 'rt',
+ 'ruby',
+ 's',
+ 'samp',
+ 'span',
+ 'strike',
+ 'strong',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr',
+ 'tt',
+ 'ul',
+ 'var',
+ ],
+ ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
+ ALLOW_DATA_ATTR: false,
+};
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index a7fcce02ab3..0f4cec67ce8 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -3,7 +3,7 @@
import katex from 'katex';
import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
-import { hasContent } from '~/lib/utils/text_utility';
+import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
@@ -140,63 +140,7 @@ export default {
markdown() {
renderer.attachments = this.cell.attachments;
- return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
- // allowedTags from GitLab's inline HTML guidelines
- // https://docs.gitlab.com/ee/user/markdown.html#inline-html
- ALLOWED_TAGS: [
- 'a',
- 'abbr',
- 'b',
- 'blockquote',
- 'br',
- 'code',
- 'dd',
- 'del',
- 'div',
- 'dl',
- 'dt',
- 'em',
- 'h1',
- 'h2',
- 'h3',
- 'h4',
- 'h5',
- 'h6',
- 'hr',
- 'i',
- 'img',
- 'ins',
- 'kbd',
- 'li',
- 'ol',
- 'p',
- 'pre',
- 'q',
- 'rp',
- 'rt',
- 'ruby',
- 's',
- 'samp',
- 'span',
- 'strike',
- 'strong',
- 'sub',
- 'summary',
- 'sup',
- 'table',
- 'tbody',
- 'td',
- 'tfoot',
- 'th',
- 'thead',
- 'tr',
- 'tt',
- 'ul',
- 'var',
- ],
- ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
- ALLOW_DATA_ATTR: false,
- });
+ return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), markdownConfig);
},
},
};
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index c91ae1ed14e..5ea431224ce 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -13,6 +13,7 @@ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
+import { renderMarkdown } from '../utils';
import {
getStartLineNumber,
getEndLineNumber,
@@ -300,7 +301,7 @@ export default {
this.isRequesting = true;
this.oldContent = this.note.note_html;
// eslint-disable-next-line vue/no-mutating-props
- this.note.note_html = escape(noteText);
+ this.note.note_html = renderMarkdown(noteText);
this.updateNote(data)
.then(() => {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7bd8d28282c..6a4a3263e4a 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -312,25 +312,23 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.notes-form .flash-container').hide(); // hide previous flash notification
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
- if (replyId) {
- if (hasQuickActions) {
- placeholderText = utils.stripQuickActions(placeholderText);
- }
+ if (hasQuickActions) {
+ placeholderText = utils.stripQuickActions(placeholderText);
+ }
- if (placeholderText.length) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- noteBody: placeholderText,
- replyId,
- });
- }
+ if (placeholderText.length) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ noteBody: placeholderText,
+ replyId,
+ });
+ }
- if (hasQuickActions) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- isSystemNote: true,
- noteBody: utils.getQuickActionText(note),
- replyId,
- });
- }
+ if (hasQuickActions) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ isSystemNote: true,
+ noteBody: utils.getQuickActionText(note),
+ replyId,
+ });
}
const processQuickActions = (res) => {
@@ -400,9 +398,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
};
const removePlaceholder = (res) => {
- if (replyId) {
- commit(types.REMOVE_PLACEHOLDER_NOTES);
- }
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
return res;
};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index b04b1d28ffa..956221d69ae 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -279,7 +279,7 @@ export const getDiscussion = (state) => (discussionId) =>
export const commentsDisabled = (state) => state.commentsDisabled;
export const suggestionsCount = (state, getters) =>
- Object.values(getters.notesById).filter((n) => n.suggestions.length).length;
+ Object.values(getters.notesById).filter((n) => n.suggestions?.length).length;
export const hasDrafts = (state, getters, rootState, rootGetters) =>
Boolean(rootGetters['batchComments/hasDrafts']);
diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js
index 7966a884eab..ec18a570960 100644
--- a/app/assets/javascripts/notes/utils.js
+++ b/app/assets/javascripts/notes/utils.js
@@ -1,4 +1,7 @@
/* eslint-disable @gitlab/require-i18n-strings */
+import marked from 'marked';
+import { sanitize } from '~/lib/dompurify';
+import { markdownConfig } from '~/lib/utils/text_utility';
/**
* Tracks snowplow event when User toggles timeline view
@@ -10,3 +13,7 @@ export const trackToggleTimelineView = (enabled) => ({
label: 'Status',
property: enabled,
});
+
+export const renderMarkdown = (rawMarkdown) => {
+ return sanitize(marked(rawMarkdown), markdownConfig);
+};
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 69afd711797..d6501a37a35 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -16,12 +16,15 @@
* :note="{body: 'This is a note'}"
* />
*/
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
+ directives: { SafeHtml },
components: {
userAvatarLink,
TimelineEntryItem,
@@ -34,6 +37,9 @@ export default {
},
computed: {
...mapGetters(['getUserData']),
+ renderedNote() {
+ return renderMarkdown(this.note.body);
+ },
},
};
</script>
@@ -57,9 +63,7 @@ export default {
</div>
</div>
<div class="note-body">
- <div class="note-text md">
- <p>{{ note.body }}</p>
- </div>
+ <div v-safe-html="renderedNote" class="note-text md"></div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index d1d27286c68..3142ef0f404 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -13,6 +13,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
feature_category :planning_analytics
+ before_action do
+ push_licensed_feature(:cycle_analytics_for_groups) if project.licensed_feature_available?(:cycle_analytics_for_groups)
+ end
+
def show
@cycle_analytics = Analytics::CycleAnalytics::ProjectLevel.new(project: @project, options: options(cycle_analytics_project_params))
diff --git a/app/graphql/types/user_callout_type.rb b/app/graphql/types/user_callout_type.rb
index 12f4fdea878..0ff32d68400 100644
--- a/app/graphql/types/user_callout_type.rb
+++ b/app/graphql/types/user_callout_type.rb
@@ -4,7 +4,7 @@ module Types
class UserCalloutType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'UserCallout'
- field :feature_name, UserCalloutFeatureNameEnum, null: false,
+ field :feature_name, UserCalloutFeatureNameEnum, null: true,
description: 'Name of the feature that the callout is for.'
field :dismissed_at, Types::TimeType, null: true,
description: 'Date when the callout was dismissed.'
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index a58994f8c86..e114e30d589 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -87,7 +87,8 @@ class Wiki
end
def create_wiki_repository
- change_head_to_default_branch if repository.create_if_not_exists
+ repository.create_if_not_exists
+ change_head_to_default_branch
raise CouldNotCreateWikiError unless repository_exists?
rescue StandardError => err
@@ -249,7 +250,7 @@ class Wiki
override :default_branch
def default_branch
- super || wiki.class.default_ref(container)
+ super || Gitlab::Git::Wiki.default_ref(container)
end
def wiki_base_path
@@ -323,6 +324,12 @@ class Wiki
end
def change_head_to_default_branch
+ # If the wiki has commits in the 'HEAD' branch means that the current
+ # HEAD is pointing to the right branch. If not, it could mean that either
+ # the repo has just been created or that 'HEAD' is pointing
+ # to the wrong branch and we need to rewrite it
+ return if repository.raw_repository.commit_count('HEAD') != 0
+
repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
end
end
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
index 82179459345..88a593cce48 100644
--- a/app/services/wikis/create_attachment_service.rb
+++ b/app/services/wikis/create_attachment_service.rb
@@ -21,7 +21,11 @@ module Wikis
end
def create_commit!
+ wiki.create_wiki_repository
+
commit_result(create_transformed_commit(@file_content))
+ rescue Wiki::CouldNotCreateWikiError
+ raise_error("Error creating the wiki repository")
end
private
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index c1f6cfc40c3..3c9762e200a 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,6 +1,6 @@
- page_title _("Value Stream Analytics")
- add_page_specific_style 'page_bundles/cycle_analytics'
- svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
-- initial_data = { request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
+- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs)
#js-cycle-analytics{ data: initial_data }
diff --git a/config/feature_flags/development/trace_memory_allocations.yml b/config/feature_flags/development/trace_memory_allocations.yml
deleted file mode 100644
index 3760ceded43..00000000000
--- a/config/feature_flags/development/trace_memory_allocations.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: trace_memory_allocations
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52306
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299524
-milestone: '13.9'
-type: development
-group: group::memory
-default_enabled: true
diff --git a/doc/administration/lfs/index.md b/doc/administration/lfs/index.md
index 793edf8322a..edf0e324a5c 100644
--- a/doc/administration/lfs/index.md
+++ b/doc/administration/lfs/index.md
@@ -321,6 +321,29 @@ end
See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19581)
+### LFS commands fail on TLS v1.3 server
+
+If you configure GitLab to [disable TLS v1.2](https://docs.gitlab.com/omnibus/settings/nginx.md)
+and only enable TLS v1.3 connections, LFS operations require a
+[Git LFS client](https://git-lfs.github.com) version 2.11.0 or later. If you use
+a Git LFS client earlier than version 2.11.0, GitLab displays an error:
+
+```plaintext
+batch response: Post https://username:***@gitlab.example.com/tool/releases.git/info/lfs/objects/batch: remote error: tls: protocol version not supported
+error: failed to fetch some objects from 'https://username:[MASKED]@gitlab.example.com/tool/releases.git/info/lfs'
+```
+
+When using GitLab CI over a TLS v1.3 configured GitLab server, you must
+[upgrade to GitLab Runner](https://docs.gitlab.com/runner/install/index.md) 13.2.0
+or later to receive an updated Git LFS client version via
+the included [GitLab Runner Helper image](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#helper-image).
+
+To check an installed Git LFS client's version, run this command:
+
+```shell
+git lfs version
+```
+
## Known limitations
- Support for removing unreferenced LFS objects was added in 8.14 onward.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index de07d601c4f..f452639d254 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -13389,7 +13389,7 @@ Represents a recorded measurement (object count) for the Admins.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="usercalloutdismissedat"></a>`dismissedAt` | [`Time`](#time) | Date when the callout was dismissed. |
-| <a id="usercalloutfeaturename"></a>`featureName` | [`UserCalloutFeatureNameEnum!`](#usercalloutfeaturenameenum) | Name of the feature that the callout is for. |
+| <a id="usercalloutfeaturename"></a>`featureName` | [`UserCalloutFeatureNameEnum`](#usercalloutfeaturenameenum) | Name of the feature that the callout is for. |
### `UserCore`
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index 6b9d5ace4e6..1e8f7f5fb81 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -157,7 +157,7 @@ export const fetchFoos = ({ state }) => {
Sometimes it's necessary to test locally what the frontend production build would produce, to do so the steps are:
1. Stop webpack: `gdk stop webpack`.
-1. Open `gitlab.yaml` located in your `gitlab` installation folder, scroll down to the `webpack` section and change `dev_server` to `enabled: false`.
+1. Open `gitlab.yaml` located in `gitlab/config` folder, scroll down to the `webpack` section, and change `dev_server` to `enabled: false`.
1. Run `yarn webpack-prod && gdk restart rails-web`.
The production build takes a few minutes to be completed. Any code changes at this point are
diff --git a/doc/development/multi_version_compatibility.md b/doc/development/multi_version_compatibility.md
index 8e49afda579..a12c01c734a 100644
--- a/doc/development/multi_version_compatibility.md
+++ b/doc/development/multi_version_compatibility.md
@@ -115,12 +115,24 @@ For major or minor version updates of Rails or Puma:
### Feature flags
-One way to handle this is to use a feature flag that is disabled by
-default. The feature flag can be enabled when the deployment is in a
-consistent state. However, this method of synchronization **does not
-guarantee** that customers with on-premise instances can [update with
-zero downtime](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates)
-because point releases bundle many changes together.
+[Feature flags](feature_flags/index.md) are a tool, not a strategy, for handling backward compatibility problems.
+
+For example, it is safe to add a new feature with frontend and API changes, if both
+frontend and API changes are disabled by default. This can be done with multiple
+merge requests, merged in any order. After all the changes are deployed to
+GitLab.com, the feature can be enabled in ChatOps and validated on GitLab.com.
+
+**However, it is not necessarily safe to enable the feature by default.** If the
+feature flag is removed, or the default is flipped to enabled, in the same release
+where the code was merged, then customers performing [zero-downtime updates](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates)
+will end up running the new frontend code against the previous release's API.
+
+If you're not sure whether it's safe to enable all the changes at once, then one
+option is to enable the API in the **current** release and enable the frontend
+change in the **next** release. This is an example of the [Expand and contract pattern](#expand-and-contract-pattern).
+
+Or you may be able to avoid delaying by a release by modifying the frontend to
+[degrade gracefully](#graceful-degradation) against the previous release's API.
### Graceful degradation
diff --git a/doc/user/clusters/agent/ci_cd_tunnel.md b/doc/user/clusters/agent/ci_cd_tunnel.md
index 3cafdabf909..67024f0a816 100644
--- a/doc/user/clusters/agent/ci_cd_tunnel.md
+++ b/doc/user/clusters/agent/ci_cd_tunnel.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# CI/CD Tunnel
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327409) in GitLab 14.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327409) in GitLab 14.1.
The CI/CD Tunnel enables users to access Kubernetes clusters from GitLab CI/CD jobs even if there is no network
connectivity between GitLab Runner and a cluster. In the current iteration, only CI/CD jobs in the Configuration project
@@ -21,7 +21,7 @@ Prerequisistes:
To create the Tunnel:
-1. In your `.gitlab-ci.yml` add a section that creates a `kubectl` compatible configuration file and use it in one
+1. In your `.gitlab-ci.yml` add a section that creates a `kubectl` compatible configuration file (`kubecontext`) and use it in one
or more jobs:
```yaml
diff --git a/doc/user/clusters/integrations.md b/doc/user/clusters/integrations.md
index dc8e75edf68..d2861ce2946 100644
--- a/doc/user/clusters/integrations.md
+++ b/doc/user/clusters/integrations.md
@@ -119,7 +119,7 @@ wget https://gitlab.com/gitlab-org/project-templates/cluster-management/-/raw/ma
helm repo add gitlab https://charts.gitlab.io
# Install Elastic Stack
-helm install prometheus gitlab/elastic-stack -n gitlab-managed-apps --values values.yaml
+helm install elastic-stack gitlab/elastic-stack -n gitlab-managed-apps --values values.yaml
```
### Enable Elastic Stack integration for your cluster
@@ -134,6 +134,6 @@ To enable the Elastic Stack integration for your cluster:
- For an [instance-level cluster](../instance/clusters/index.md), navigate to your instance's
**Kubernetes** page.
1. Select the **Integrations** tab.
-1. Check the **Enable Prometheus integration** checkbox.
+1. Check the **Enable Elastic Stack integration** checkbox.
1. Click **Save changes**.
1. Go to the **Health** tab to see your cluster's metrics.
diff --git a/doc/user/project/import/fogbugz.md b/doc/user/project/import/fogbugz.md
index d3d77f16200..982bc6d90e8 100644
--- a/doc/user/project/import/fogbugz.md
+++ b/doc/user/project/import/fogbugz.md
@@ -7,31 +7,24 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Import your project from FogBugz to GitLab **(FREE)**
-It only takes a few simple steps to import your project from FogBugz.
-The importer imports all your cases and comments with original case
-numbers and timestamps. You can also map FogBugz users to GitLab users.
+Using the importer, you can import your FogBugz project to GitLab.com
+or to your self-managed GitLab instance.
-Follow these steps to import your project from FogBugz:
+The importer imports all of your cases and comments with the original
+case numbers and timestamps. You can also map FogBugz users to GitLab
+users.
-1. From your GitLab dashboard, select **New project**.
+To import your project from FogBugz:
+1. From your GitLab dashboard, select **New project**.
1. Select the **FogBugz** button.
-
- ![FogBugz](img/fogbugz_import_select_fogbogz.png)
-
+ ![FogBugz](img/fogbugz_import_select_fogbogz.png)
1. Enter your FogBugz URL, email address, and password.
-
- ![Login](img/fogbugz_import_login.png)
-
-1. Create mapping from FogBugz users to GitLab users.
-
- ![User Map](img/fogbugz_import_user_map.png)
-
-1. Select the projects you wish to import by selecting the **Import** buttons.
-
- ![Import Project](img/fogbugz_import_select_project.png)
-
-1. Once the import finishes, click the link to go to the project
+ ![Login](img/fogbugz_import_login.png)
+1. Create a mapping from FogBugz users to GitLab users.
+ ![User Map](img/fogbugz_import_user_map.png)
+1. Select **Import** for the projects you want to import.
+ ![Import Project](img/fogbugz_import_select_project.png)
+1. After the import finishes, click the link to go to the project
dashboard. Follow the directions to push your existing repository.
-
- ![Finished](img/fogbugz_import_finished.png)
+ ![Finished](img/fogbugz_import_finished.png)
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index d46b3a27e72..7f755b1a4d4 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -7,7 +7,7 @@ module API
before { authenticate! }
- feature_category :continuous_integration
+ feature_category :runner
resource :runners do
desc 'Get runners available for user' do
diff --git a/lib/gitlab/memory/instrumentation.rb b/lib/gitlab/memory/instrumentation.rb
index e800fe14cf1..4e93d6a9ece 100644
--- a/lib/gitlab/memory/instrumentation.rb
+++ b/lib/gitlab/memory/instrumentation.rb
@@ -21,24 +21,13 @@ module Gitlab
Thread.current.respond_to?(:memory_allocations)
end
- # This method changes a global state
- def self.ensure_feature_flag!
+ def self.start_thread_memory_allocations
return unless available?
- enabled = Feature.enabled?(:trace_memory_allocations, default_enabled: true)
- return if enabled == Thread.trace_memory_allocations
-
MUTEX.synchronize do
- # This enables or disables feature dynamically
- # based on a feature flag
- Thread.trace_memory_allocations = enabled
+ # This method changes a global state
+ Thread.trace_memory_allocations = true
end
- end
-
- def self.start_thread_memory_allocations
- return unless available?
-
- ensure_feature_flag!
# it will return `nil` if disabled
Thread.current.memory_allocations
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index 2a231f8fce0..597df9936ea 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -117,7 +117,7 @@ module Gitlab
private
def track(values, event_name, context: '', time: Time.zone.now)
- return unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return unless usage_ping_enabled?
event = event_for(event_name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownEvent.new("Unknown event #{event_name}")) unless event.present?
@@ -131,6 +131,10 @@ module Gitlab
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
end
+ def usage_ping_enabled?
+ Gitlab::CurrentSettings.usage_ping_enabled?
+ end
+
# The array of valid context on which we allow tracking
def valid_context_list
Plan.all_plans
diff --git a/package.json b/package.json
index 3238a8b1f4b..b8773f4329a 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.202.0",
"@gitlab/tributejs": "1.0.0",
- "@gitlab/ui": "31.3.0",
+ "@gitlab/ui": "31.5.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.3-2",
"@rails/ujs": "6.1.3-2",
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index 1fcb40df491..c646698219b 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe 'Merge request > Batch comments', :js do
end
before do
+ stub_feature_flags(paginated_notes: false)
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 9b6385dc523..4e6471d5f7b 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -174,6 +174,15 @@ export const stageMedians = {
staging: 388800,
};
+export const formattedStageMedians = {
+ issue: '2d',
+ plan: '1d',
+ review: '1w',
+ code: '1d',
+ test: '3d',
+ staging: '4d',
+};
+
export const allowedStages = [issueStage, planStage, codeStage];
export const transformedProjectStagePathData = [
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 4f37e1266fb..8a8dd374f8e 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import httpStatusCodes from '~/lib/utils/http_status';
-import { selectedStage, selectedValueStream } from '../mock_data';
+import { allowedStages, selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path';
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
@@ -25,6 +25,10 @@ const mockRequestedDataMutations = [
},
];
+const features = {
+ cycleAnalyticsForGroups: true,
+};
+
describe('Project Value Stream Analytics actions', () => {
let state;
let mock;
@@ -175,6 +179,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
+ features,
fullPath: mockFullPath,
};
mock = new MockAdapter(axios);
@@ -187,9 +192,33 @@ describe('Project Value Stream Analytics actions', () => {
state,
payload: {},
expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
- expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
+ expectedActions: [
+ { type: 'receiveValueStreamsSuccess' },
+ { type: 'setSelectedStage' },
+ { type: 'fetchStageMedians' },
+ ],
}));
+ describe('with cycleAnalyticsForGroups=false', () => {
+ beforeEach(() => {
+ state = {
+ features: { cycleAnalyticsForGroups: false },
+ fullPath: mockFullPath,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it("does not dispatch the 'fetchStageMedians' request", () =>
+ testAction({
+ action: actions.fetchValueStreams,
+ state,
+ payload: {},
+ expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
+ expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
+ }));
+ });
+
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -280,4 +309,59 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
+
+ describe('fetchStageMedians', () => {
+ const mockValueStreamPath = /median/;
+
+ const stageMediansPayload = [
+ { id: 'issue', value: null },
+ { id: 'plan', value: null },
+ { id: 'code', value: null },
+ ];
+
+ const stageMedianError = new Error(
+ `Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
+ );
+
+ beforeEach(() => {
+ state = {
+ fullPath: mockFullPath,
+ selectedValueStream,
+ stages: allowedStages,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it(`commits the 'REQUEST_STAGE_MEDIANS' and 'RECEIVE_STAGE_MEDIANS_SUCCESS' mutations`, () =>
+ testAction({
+ action: actions.fetchStageMedians,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_MEDIANS' },
+ { type: 'RECEIVE_STAGE_MEDIANS_SUCCESS', payload: stageMediansPayload },
+ ],
+ expectedActions: [],
+ }));
+
+ describe('with a failing request', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ });
+
+ it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
+ testAction({
+ action: actions.fetchStageMedians,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_MEDIANS' },
+ { type: 'RECEIVE_STAGE_MEDIANS_ERROR', payload: stageMedianError },
+ ],
+ expectedActions: [],
+ }));
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 88e1a13f506..77b19280517 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -1,3 +1,5 @@
+import { useFakeDate } from 'helpers/fake_date';
+import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants';
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
import {
@@ -9,15 +11,23 @@ import {
selectedValueStream,
rawValueStreamStages,
valueStreamStages,
+ rawStageMedians,
+ formattedStageMedians,
} from '../mock_data';
let state;
const mockRequestPath = 'fake/request/path';
-const mockStartData = '2021-04-20';
+const mockCreatedAfter = '2020-06-18';
+const mockCreatedBefore = '2020-07-18';
+const features = {
+ cycleAnalyticsForGroups: true,
+};
describe('Project Value Stream Analytics mutations', () => {
+ useFakeDate(2020, 6, 18);
+
beforeEach(() => {
- state = {};
+ state = { features };
});
afterEach(() => {
@@ -46,6 +56,8 @@ describe('Project Value Stream Analytics mutations', () => {
${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'hasError'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
+ ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
+ ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
`('$mutation will set $stateKey to $value', ({ mutation, stateKey, value }) => {
mutations[mutation](state, {});
@@ -53,15 +65,19 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | payload | stateKey | value
- ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
- ${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
- ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
- ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
- ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
- ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
- ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
+ mutation | payload | stateKey | value
+ ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'startDate'} | ${DEFAULT_DAYS_TO_DISPLAY}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdAfter'} | ${mockCreatedAfter}
+ ${types.SET_DATE_RANGE} | ${{ startDate: DEFAULT_DAYS_TO_DISPLAY }} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
+ ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
+ ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
@@ -92,4 +108,35 @@ describe('Project Value Stream Analytics mutations', () => {
},
);
});
+
+ describe('with cycleAnalyticsForGroups=false', () => {
+ useFakeDate(2020, 6, 18);
+
+ beforeEach(() => {
+ state = { features: { cycleAnalyticsForGroups: false } };
+ });
+
+ const formattedMedians = {
+ code: '2d',
+ issue: '-',
+ plan: '21h',
+ review: '-',
+ staging: '2d',
+ test: '4h',
+ };
+
+ it.each`
+ mutation | payload | stateKey | value
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'medians'} | ${formattedMedians}
+ ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${{}} | ${'medians'} | ${{}}
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${{}} | ${'medians'} | ${{}}
+ `(
+ '$mutation with $payload will set $stateKey to $value',
+ ({ mutation, payload, stateKey, value }) => {
+ mutations[mutation](state, payload);
+
+ expect(state).toMatchObject({ [stateKey]: value });
+ },
+ );
+ });
});
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 15137bb0571..1fecdfc0539 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,3 +1,4 @@
+import { useFakeDate } from 'helpers/fake_date';
import {
decorateEvents,
decorateData,
@@ -6,6 +7,7 @@ import {
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
+ calculateFormattedDayInPast,
} from '~/cycle_analytics/utils';
import {
selectedStage,
@@ -149,4 +151,12 @@ describe('Value stream analytics utils', () => {
expect(filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result);
});
});
+
+ describe('calculateFormattedDayInPast', () => {
+ useFakeDate(1815, 11, 10);
+
+ it('will return 2 dates, now and past', () => {
+ expect(calculateFormattedDayInPast(5)).toEqual({ now: '1815-12-10', past: '1815-12-05' });
+ });
+ });
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 7444c441e06..f217dfd2e48 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import { escape } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -263,7 +262,9 @@ describe('issue_note', () => {
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.vm.note.note_html).toBe(escape(noteBody));
+ expect(wrapper.vm.note.note_html).toBe(
+ '<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n',
+ );
});
});
@@ -291,7 +292,7 @@ describe('issue_note', () => {
await wrapper.vm.$nextTick();
let noteBodyProps = noteBody.props();
- expect(noteBodyProps.note.note_html).toBe(updatedText);
+ expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`);
noteBody.vm.$emit('cancelForm');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index f3ce03796f9..5e956d66b6a 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -55,6 +55,8 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
<p>
Foo
</p>
+
+
</div>
</div>
</div>
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index e6cc0174642..48fcc9f93db 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -99,23 +99,6 @@ RSpec.describe Gitlab::InstrumentationHelper do
:mem_mallocs
)
end
-
- context 'when trace_memory_allocations is disabled' do
- before do
- stub_feature_flags(trace_memory_allocations: false)
- Gitlab::Memory::Instrumentation.ensure_feature_flag!
- end
-
- it 'does not log memory usage metrics' do
- subject
-
- expect(payload).not_to include(
- :mem_objects,
- :mem_bytes,
- :mem_mallocs
- )
- end
- end
end
context 'when load balancing is enabled' do
diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb
index 0dbe9a8e275..069c45da18a 100644
--- a/spec/lib/gitlab/memory/instrumentation_spec.rb
+++ b/spec/lib/gitlab/memory/instrumentation_spec.rb
@@ -18,24 +18,8 @@ RSpec.describe Gitlab::Memory::Instrumentation do
describe '.start_thread_memory_allocations' do
subject { described_class.start_thread_memory_allocations }
- context 'when feature flag trace_memory_allocations is enabled' do
- before do
- stub_feature_flags(trace_memory_allocations: true)
- end
-
- it 'a hash is returned' do
- is_expected.not_to be_empty
- end
- end
-
- context 'when feature flag trace_memory_allocations is disabled' do
- before do
- stub_feature_flags(trace_memory_allocations: false)
- end
-
- it 'a nil is returned' do
- is_expected.to be_nil
- end
+ it 'a hash is returned' do
+ is_expected.to be_a(Hash)
end
context 'when feature is unavailable' do
@@ -63,30 +47,14 @@ RSpec.describe Gitlab::Memory::Instrumentation do
expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original
end
- context 'when feature flag trace_memory_allocations is enabled' do
- before do
- stub_feature_flags(trace_memory_allocations: true)
- end
-
- it 'a hash is returned' do
- result = subject
- expect(result).to include(
- mem_objects: be > 1000,
- mem_mallocs: be > 1000,
- mem_bytes: be > 100_000, # 100 items * 100 bytes each
- mem_total_bytes: eq(result[:mem_bytes] + 40 * result[:mem_objects])
- )
- end
- end
-
- context 'when feature flag trace_memory_allocations is disabled' do
- before do
- stub_feature_flags(trace_memory_allocations: false)
- end
-
- it 'a nil is returned' do
- is_expected.to be_nil
- end
+ it 'a hash is returned' do
+ result = subject
+ expect(result).to include(
+ mem_objects: be > 1000,
+ mem_mallocs: be > 1000,
+ mem_bytes: be > 100_000, # 100 items * 100 bytes each
+ mem_total_bytes: eq(result[:mem_bytes] + 40 * result[:mem_objects])
+ )
end
context 'when feature is unavailable' do
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 17b2c7b38e1..6c356f685ec 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -318,7 +318,8 @@ RSpec.describe EventCreateService do
specify { expect { result }.to change { Event.count }.by(8) }
- specify { expect { result }.not_to exceed_query_limit(1) }
+ # An addditional query due to event tracking
+ specify { expect { result }.not_to exceed_query_limit(2) }
it 'creates 3 created design events' do
ids = result.pluck('id')
@@ -347,7 +348,8 @@ RSpec.describe EventCreateService do
specify { expect { result }.to change { Event.count }.by(5) }
- specify { expect { result }.not_to exceed_query_limit(1) }
+ # An addditional query due to event tracking
+ specify { expect { result }.not_to exceed_query_limit(2) }
it 'creates 5 destroyed design events' do
ids = result.pluck('id')
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 06bab24bb0e..bc5956e3eec 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -575,18 +575,21 @@ RSpec.shared_examples 'wiki model' do
end
describe '#create_wiki_repository' do
+ let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') }
+ let(:default_branch) { 'foo' }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
+ end
+
subject { wiki.create_wiki_repository }
context 'when repository is not created' do
let(:wiki_container) { wiki_container_without_repo }
- let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') }
- let(:default_branch) { 'foo' }
it 'changes the HEAD reference to the default branch' do
expect(wiki.empty?).to eq true
- allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
-
subject
expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
@@ -596,23 +599,36 @@ RSpec.shared_examples 'wiki model' do
context 'when repository is empty' do
let(:wiki_container) { wiki_container_without_repo }
- it 'does nothing' do
+ it 'changes the HEAD reference to the default branch' do
wiki.repository.create_if_not_exists
-
- expect(wiki).not_to receive(:change_head_to_default_branch)
+ wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar')
subject
+
+ expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
end
end
context 'when repository is not empty' do
- it 'does nothing' do
+ before do
wiki.create_page('index', 'test content')
+ end
- expect(wiki).not_to receive(:change_head_to_default_branch)
+ it 'does nothing when HEAD points to the right branch' do
+ expect(wiki.repository.raw_repository).not_to receive(:write_ref)
subject
end
+
+ context 'when HEAD points to the wrong branch' do
+ it 'rewrites HEAD with the right branch' do
+ wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar')
+
+ subject
+
+ expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
index 555a6d5eed0..1646c18a0ed 100644
--- a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
@@ -25,6 +25,25 @@ RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_typ
container.add_developer(user)
end
+ context 'creates wiki repository if it does not exist' do
+ let(:container) { create(container_type) } # rubocop:disable Rails/SaveBang
+
+ it 'creates wiki repository' do
+ expect { service.execute }.to change { container.wiki.repository.exists? }.to(true)
+ end
+
+ context 'if an error is raised creating the repository' do
+ it 'catches error and return gracefully' do
+ allow(container.wiki).to receive(:repository_exists?).and_return(false)
+
+ result = service.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'Error creating the wiki repository'
+ end
+ end
+ end
+
context 'creates branch if it does not exists' do
let(:branch_name) { 'new_branch' }
let(:opts) { file_opts.merge(branch_name: branch_name) }
diff --git a/yarn.lock b/yarn.lock
index 93d06635536..c316b7d9d00 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -908,10 +908,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
-"@gitlab/ui@31.3.0":
- version "31.3.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.3.0.tgz#90619e2b52e68100323488042fcbd1b4c1226b54"
- integrity sha512-FsClTQHQZqPQ8cYod1wAx7T3Ogs77PIQ+4EnD2HGZxXTnQHmM3JRivRcp15F+tOW6vJbeNJmHNLT/d03W8G4kg==
+"@gitlab/ui@31.5.0":
+ version "31.5.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.5.0.tgz#45b7866b790e7d5a1b67b39000c047991036b437"
+ integrity sha512-pIJXbO0zfpgD0CmZTjKMMCu6l1AMLWiGPdPqDhqgGskuKGOU1UxX7ApDLesgRsSCievMTD/zsVvRHrG6AH7LiQ==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "2.18.1"