Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c8df22c555
commit
fc1df8c830
39
.vale.ini
39
.vale.ini
|
@ -1,40 +1,9 @@
|
||||||
# Vale configuration file, taken from https://errata-ai.github.io/vale/config/
|
# Vale configuration file.
|
||||||
|
#
|
||||||
|
# For more information, see https://errata-ai.gitbook.io/vale/getting-started/configuration.
|
||||||
|
|
||||||
# The relative path to the folder containing linting rules (styles)
|
StylesPath = doc/.vale
|
||||||
# -----------------------------------------------------------------
|
|
||||||
StylesPath = doc/.linting/vale/styles
|
|
||||||
|
|
||||||
# Minimum alert level
|
|
||||||
# -------------------
|
|
||||||
# The minimum alert level to display (suggestion, warning, or error).
|
|
||||||
# If integrated into CI, builds fail by default on error-level alerts,
|
|
||||||
# unless you execute Vale with the --no-exit flag
|
|
||||||
MinAlertLevel = suggestion
|
MinAlertLevel = suggestion
|
||||||
|
|
||||||
# Should Vale parse any file formats other than .md files as Markdown?
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
[formats]
|
|
||||||
mdx = md
|
|
||||||
|
|
||||||
# What file types should Vale test?
|
|
||||||
# ----------------------------------
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
|
||||||
# Styles to load
|
|
||||||
# --------------
|
|
||||||
# What styles, located in the StylesPath folder, should Vale load?
|
|
||||||
# Vale also currently includes write-good, proselint, joblint, and vale
|
|
||||||
BasedOnStyles = gitlab
|
BasedOnStyles = gitlab
|
||||||
|
|
||||||
# Enabling or disabling specific rules in a style
|
|
||||||
# -----------------------------------------------
|
|
||||||
# To disable a rule in an enabled style, use the following format:
|
|
||||||
# {style}.{filename} = NO
|
|
||||||
# To enable a single rule in a disabled style, use the following format:
|
|
||||||
# vale.Editorializing = YES
|
|
||||||
|
|
||||||
# Altering the severity of a rule in a style
|
|
||||||
# ------------------------------------------
|
|
||||||
# To change the reporting level (suggestion, warning, error) of a rule,
|
|
||||||
# use the following format: {style}.{filename} = {level}
|
|
||||||
# vale.Hedging = error
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import _ from 'underscore';
|
|
||||||
import axios from './lib/utils/axios_utils';
|
import axios from './lib/utils/axios_utils';
|
||||||
import { joinPaths } from './lib/utils/url_utility';
|
import { joinPaths } from './lib/utils/url_utility';
|
||||||
import flash from '~/flash';
|
import flash from '~/flash';
|
||||||
|
@ -70,7 +68,7 @@ const Api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Return groups list. Filtered by query
|
// Return groups list. Filtered by query
|
||||||
groups(query, options, callback = $.noop) {
|
groups(query, options, callback = () => {}) {
|
||||||
const url = Api.buildUrl(Api.groupsPath);
|
const url = Api.buildUrl(Api.groupsPath);
|
||||||
return axios
|
return axios
|
||||||
.get(url, {
|
.get(url, {
|
||||||
|
@ -108,7 +106,7 @@ const Api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Return projects list. Filtered by query
|
// Return projects list. Filtered by query
|
||||||
projects(query, options, callback = _.noop) {
|
projects(query, options, callback = () => {}) {
|
||||||
const url = Api.buildUrl(Api.projectsPath);
|
const url = Api.buildUrl(Api.projectsPath);
|
||||||
const defaults = {
|
const defaults = {
|
||||||
search: query,
|
search: query,
|
||||||
|
|
|
@ -26,15 +26,17 @@ export default {
|
||||||
modalInfo: {
|
modalInfo: {
|
||||||
closeText: s__('EnableReviewApp|Close'),
|
closeText: s__('EnableReviewApp|Close'),
|
||||||
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
|
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
|
||||||
copyString: `deploy_review
|
copyString: `deploy_review:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
- echo "Deploy a review app"
|
- echo "Deploy a review app"
|
||||||
environment:
|
environment:
|
||||||
name: review/$CI_COMMIT_REF_NAME
|
name: review/$CI_COMMIT_REF_NAME
|
||||||
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
||||||
only: branches
|
only:
|
||||||
except: master`,
|
- branches
|
||||||
|
except:
|
||||||
|
- master`,
|
||||||
id: 'enable-review-app-info',
|
id: 'enable-review-app-info',
|
||||||
title: s__('ReviewApp|Enable Review App'),
|
title: s__('ReviewApp|Enable Review App'),
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default {
|
||||||
data-boundary="viewport"
|
data-boundary="viewport"
|
||||||
@click="openDiscardModal"
|
@click="openDiscardModal"
|
||||||
>
|
>
|
||||||
<icon :size="16" name="remove-all" class="ml-auto mr-auto" />
|
<icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default {
|
||||||
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
|
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
|
||||||
<template v-else-if="hasLoadedPipeline">
|
<template v-else-if="hasLoadedPipeline">
|
||||||
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
|
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
|
||||||
<ci-icon :status="latestPipeline.details.status" :size="24" />
|
<ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
|
||||||
<span class="prepend-left-8">
|
<span class="prepend-left-8">
|
||||||
<strong> {{ __('Pipeline') }} </strong>
|
<strong> {{ __('Pipeline') }} </strong>
|
||||||
<a
|
<a
|
||||||
|
@ -76,6 +76,7 @@ export default {
|
||||||
:help-page-path="links.ciHelpPagePath"
|
:help-page-path="links.ciHelpPagePath"
|
||||||
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
|
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
|
||||||
:can-set-ci="true"
|
:can-set-ci="true"
|
||||||
|
class="mb-auto mt-auto"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
|
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
|
||||||
<p class="append-bottom-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
|
<p class="append-bottom-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
|
||||||
|
|
|
@ -1,18 +1,40 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
import { memoize } from 'lodash';
|
||||||
|
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve SVG icon path content from gitlab/svg sprite icons
|
* Resolves to a DOM that contains GitLab icons
|
||||||
* @param {String} name
|
* in svg format. Memoized to avoid duplicate requests
|
||||||
*/
|
*/
|
||||||
export const getSvgIconPathContent = name =>
|
const getSvgDom = memoize(() =>
|
||||||
axios
|
axios
|
||||||
.get(gon.sprite_icons)
|
.get(gon.sprite_icons)
|
||||||
.then(({ data: svgs }) =>
|
.then(({ data: svgs }) => new DOMParser().parseFromString(svgs, 'text/xml'))
|
||||||
new DOMParser()
|
.catch(() => {
|
||||||
.parseFromString(svgs, 'text/xml')
|
getSvgDom.cache.clear();
|
||||||
.querySelector(`#${name} path`)
|
}),
|
||||||
.getAttribute('d'),
|
);
|
||||||
)
|
|
||||||
|
/**
|
||||||
|
* Clears the memoized SVG content.
|
||||||
|
*
|
||||||
|
* You probably don't need to invoke this function unless
|
||||||
|
* sprite_icons are updated.
|
||||||
|
*/
|
||||||
|
export const clearSvgIconPathContentCache = () => {
|
||||||
|
getSvgDom.cache.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve SVG icon path content from gitlab/svg sprite icons.
|
||||||
|
*
|
||||||
|
* Content loaded is cached.
|
||||||
|
*
|
||||||
|
* @param {String} name - Icon name
|
||||||
|
* @returns A promise that resolves to the svg path
|
||||||
|
*/
|
||||||
|
export const getSvgIconPathContent = name =>
|
||||||
|
getSvgDom()
|
||||||
|
.then(doc => {
|
||||||
|
return doc.querySelector(`#${name} path`).getAttribute('d');
|
||||||
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
|
||||||
|
// stylelint-disable selector-class-pattern
|
||||||
|
// stylelint-disable selector-max-compound-selectors
|
||||||
|
// stylelint-disable stylelint-gitlab/duplicate-selectors
|
||||||
|
// stylelint-disable stylelint-gitlab/utility-classes
|
||||||
|
|
||||||
|
.blob-editor-container {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.vertical-center {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor .lines-content .cigr {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor .selected-text {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor .view-lines {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-readonly,
|
||||||
|
.editor.original {
|
||||||
|
.view-lines {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursors-layer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-deleted {
|
||||||
|
.editor.modified {
|
||||||
|
.margin-view-overlays,
|
||||||
|
.lines-content,
|
||||||
|
.decorationsOverviewRuler {
|
||||||
|
// !important to override monaco inline styles
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffOverviewRuler.modified {
|
||||||
|
// !important to override monaco inline styles
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-added {
|
||||||
|
.editor.original {
|
||||||
|
.margin-view-overlays,
|
||||||
|
.lines-content,
|
||||||
|
.decorationsOverviewRuler {
|
||||||
|
// !important to override monaco inline styles
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffOverviewRuler.original {
|
||||||
|
// !important to override monaco inline styles
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-diff-editor.vs {
|
||||||
|
.editor.modified {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagonal-fill {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffOverview {
|
||||||
|
background-color: $white-light;
|
||||||
|
border-left: 1px solid $white-dark;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diffViewport {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-insert {
|
||||||
|
background-color: $line-added-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-delete {
|
||||||
|
background-color: $line-removed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers {
|
||||||
|
color: $black-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-overlays {
|
||||||
|
.line-insert {
|
||||||
|
background-color: $line-added;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-delete {
|
||||||
|
background-color: $line-removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin {
|
||||||
|
background-color: $white-light;
|
||||||
|
border-right: 1px solid $gray-100;
|
||||||
|
|
||||||
|
.line-insert {
|
||||||
|
border-right: 1px solid $line-added-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-delete {
|
||||||
|
border-right: 1px solid $line-removed-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-view-overlays .insert-sign,
|
||||||
|
.margin-view-overlays .delete-sign {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-file-editor-holder {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0; // firefox fix
|
||||||
|
|
||||||
|
&.is-readonly .vs,
|
||||||
|
.vs .editor.original {
|
||||||
|
.monaco-editor,
|
||||||
|
.monaco-editor-background,
|
||||||
|
.monaco-editor .inputarea.ime-input {
|
||||||
|
background-color: $gray-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
@import 'framework/variables';
|
@import 'framework/variables';
|
||||||
@import 'framework/mixins';
|
@import 'framework/mixins';
|
||||||
@import './ide_mixins';
|
@import './ide_mixins';
|
||||||
|
@import './ide_monaco_overrides';
|
||||||
|
|
||||||
$search-list-icon-width: 18px;
|
$search-list-icon-width: 18px;
|
||||||
$ide-activity-bar-width: 60px;
|
$ide-activity-bar-width: 60px;
|
||||||
|
@ -16,11 +17,6 @@ $ide-commit-header-height: 48px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-message {
|
.commit-message {
|
||||||
@include str-truncated(250px);
|
@include str-truncated(250px);
|
||||||
}
|
}
|
||||||
|
@ -49,10 +45,6 @@ $ide-commit-header-height: 48px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0; // firefox fix
|
min-height: 0; // firefox fix
|
||||||
|
|
||||||
a {
|
|
||||||
color: $gl-text-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-file-loading-container {
|
.multi-file-loading-container {
|
||||||
|
@ -160,157 +152,6 @@ $ide-commit-header-height: 48px;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// stylelint-disable selector-class-pattern
|
|
||||||
// stylelint-disable selector-max-compound-selectors
|
|
||||||
// stylelint-disable stylelint-gitlab/duplicate-selectors
|
|
||||||
// stylelint-disable stylelint-gitlab/utility-classes
|
|
||||||
|
|
||||||
.blob-editor-container {
|
|
||||||
flex: 1;
|
|
||||||
height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.vertical-center {
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor .lines-content .cigr {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor .selected-text {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor .view-lines {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-readonly,
|
|
||||||
.editor.original {
|
|
||||||
.view-lines {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cursors-layer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-deleted {
|
|
||||||
.editor.modified {
|
|
||||||
.margin-view-overlays,
|
|
||||||
.lines-content,
|
|
||||||
.decorationsOverviewRuler {
|
|
||||||
// !important to override monaco inline styles
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.diffOverviewRuler.modified {
|
|
||||||
// !important to override monaco inline styles
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-added {
|
|
||||||
.editor.original {
|
|
||||||
.margin-view-overlays,
|
|
||||||
.lines-content,
|
|
||||||
.decorationsOverviewRuler {
|
|
||||||
// !important to override monaco inline styles
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.diffOverviewRuler.original {
|
|
||||||
// !important to override monaco inline styles
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-diff-editor.vs {
|
|
||||||
.editor.modified {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagonal-fill {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diffOverview {
|
|
||||||
background-color: $white-light;
|
|
||||||
border-left: 1px solid $white-dark;
|
|
||||||
cursor: ns-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diffViewport {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.char-insert {
|
|
||||||
background-color: $line-added-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.char-delete {
|
|
||||||
background-color: $line-removed-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-numbers {
|
|
||||||
color: $black-transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-overlays {
|
|
||||||
.line-insert {
|
|
||||||
background-color: $line-added;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-delete {
|
|
||||||
background-color: $line-removed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin {
|
|
||||||
background-color: $white-light;
|
|
||||||
border-right: 1px solid $gray-100;
|
|
||||||
|
|
||||||
.line-insert {
|
|
||||||
border-right: 1px solid $line-added-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-delete {
|
|
||||||
border-right: 1px solid $line-removed-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.margin-view-overlays .insert-sign,
|
|
||||||
.margin-view-overlays .delete-sign {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multi-file-editor-holder {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0; // firefox fix
|
|
||||||
|
|
||||||
&.is-readonly .vs,
|
|
||||||
.vs .editor.original {
|
|
||||||
.monaco-editor,
|
|
||||||
.monaco-editor-background,
|
|
||||||
.monaco-editor .inputarea.ime-input {
|
|
||||||
background-color: $gray-50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stylelint-enable selector-class-pattern
|
|
||||||
// stylelint-enable selector-max-compound-selectors
|
|
||||||
// stylelint-enable stylelint-gitlab/duplicate-selectors
|
|
||||||
// stylelint-enable stylelint-gitlab/utility-classes
|
|
||||||
|
|
||||||
.preview-container {
|
.preview-container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -671,10 +512,6 @@ $ide-commit-header-height: 48px;
|
||||||
width: $ide-commit-row-height;
|
width: $ide-commit-row-height;
|
||||||
height: $ide-commit-row-height;
|
height: $ide-commit-row-height;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
> svg {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-commit-file-count {
|
.ide-commit-file-count {
|
||||||
|
@ -864,39 +701,39 @@ $ide-commit-header-height: 48px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-nav-dropdown {
|
button {
|
||||||
width: 100%;
|
color: $gl-text-color;
|
||||||
margin-bottom: 12px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.ide-nav-dropdown {
|
||||||
width: 385px;
|
width: 100%;
|
||||||
max-height: initial;
|
margin-bottom: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu-toggle {
|
.dropdown-menu {
|
||||||
svg {
|
width: 385px;
|
||||||
vertical-align: middle;
|
max-height: initial;
|
||||||
color: $gray-700;
|
}
|
||||||
|
|
||||||
&:hover {
|
.dropdown-menu-toggle {
|
||||||
color: $gray-700;
|
svg {
|
||||||
}
|
vertical-align: middle;
|
||||||
}
|
color: $gray-700;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $white-normal;
|
color: $gray-700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.show {
|
&:hover {
|
||||||
.dropdown-menu-toggle {
|
background-color: $white-normal;
|
||||||
background-color: $white-dark;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
&.show {
|
||||||
color: $gl-text-color;
|
.dropdown-menu-toggle {
|
||||||
|
background-color: $white-dark;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -945,6 +782,8 @@ $ide-commit-header-height: 48px;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-enter,
|
||||||
|
.fade-leave-to,
|
||||||
.commit-form-slide-up-enter,
|
.commit-form-slide-up-enter,
|
||||||
.commit-form-slide-up-leave-to {
|
.commit-form-slide-up-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -1063,9 +902,6 @@ $ide-commit-header-height: 48px;
|
||||||
@include ide-trace-view();
|
@include ide-trace-view();
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: auto;
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: $grid-size 0;
|
margin: $grid-size 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1092,10 +928,6 @@ $ide-commit-header-height: 48px;
|
||||||
min-height: 55px;
|
min-height: 55px;
|
||||||
padding-left: $gl-padding;
|
padding-left: $gl-padding;
|
||||||
padding-right: $gl-padding;
|
padding-right: $gl-padding;
|
||||||
|
|
||||||
.ci-status-icon {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-job-item {
|
.ide-job-item {
|
||||||
|
@ -1135,7 +967,7 @@ $ide-commit-header-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-nav-form {
|
.ide-nav-form {
|
||||||
.nav-links li {
|
li {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
@ -1222,10 +1054,6 @@ $ide-commit-header-height: 48px;
|
||||||
background-color: $blue-500;
|
background-color: $blue-500;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: currentColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-new-btn {
|
.ide-new-btn {
|
||||||
|
|
|
@ -241,6 +241,10 @@ module SystemNoteService
|
||||||
def zoom_link_removed(issue, project, author)
|
def zoom_link_removed(issue, project, author)
|
||||||
::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_removed
|
::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_removed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_resolve_prometheus_alert(noteable, project, author)
|
||||||
|
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).auto_resolve_prometheus_alert
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SystemNoteService.prepend_if_ee('EE::SystemNoteService')
|
SystemNoteService.prepend_if_ee('EE::SystemNoteService')
|
||||||
|
|
|
@ -288,6 +288,12 @@ module SystemNotes
|
||||||
create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
|
create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_resolve_prometheus_alert
|
||||||
|
body = 'automatically closed this issue because the alert resolved.'
|
||||||
|
|
||||||
|
create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def cross_reference_note_content(gfm_reference)
|
def cross_reference_note_content(gfm_reference)
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: 'Fixes stop_review job upon expired artifacts from previous stages'
|
||||||
|
merge_request: 27258
|
||||||
|
author: Jack Lei
|
||||||
|
type: fixed
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
@ -81,6 +81,8 @@ already reserved for category labels).
|
||||||
The descriptions on the [labels page](https://gitlab.com/groups/gitlab-org/-/labels)
|
The descriptions on the [labels page](https://gitlab.com/groups/gitlab-org/-/labels)
|
||||||
explain what falls under each type label.
|
explain what falls under each type label.
|
||||||
|
|
||||||
|
The GitLab handbook documents [when something is a bug and when it is a feature request.](https://about.gitlab.com/handbook/product/product-management/process/feature-or-bug.html)
|
||||||
|
|
||||||
### Facet labels
|
### Facet labels
|
||||||
|
|
||||||
Sometimes it's useful to refine the type of an issue. In those cases, you can
|
Sometimes it's useful to refine the type of an issue. In those cases, you can
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
redirect_to: '../../telemetry/backend.md'
|
||||||
|
---
|
||||||
|
|
||||||
|
This document was moved to [another location](../../telemetry/backend.md).
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
redirect_to: '../../telemetry/frontend.md'
|
||||||
|
---
|
||||||
|
|
||||||
|
This document was moved to [another location](../../telemetry/frontend.md).
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
redirect_to: '../../telemetry/index.md'
|
||||||
|
---
|
||||||
|
|
||||||
|
This document was moved to [another location](../../telemetry/index.md).
|
|
@ -127,7 +127,7 @@ one major version. For example, it is safe to:
|
||||||
- `9.5.5` -> `9.5.9`
|
- `9.5.5` -> `9.5.9`
|
||||||
- `10.6.3` -> `10.6.6`
|
- `10.6.3` -> `10.6.6`
|
||||||
- `11.11.1` -> `11.11.8`
|
- `11.11.1` -> `11.11.8`
|
||||||
- `12.0.4` -> `12.0.9`
|
- `12.0.4` -> `12.0.12`
|
||||||
- Upgrade the minor version:
|
- Upgrade the minor version:
|
||||||
- `8.9.4` -> `8.12.3`
|
- `8.9.4` -> `8.12.3`
|
||||||
- `9.2.3` -> `9.5.5`
|
- `9.2.3` -> `9.5.5`
|
||||||
|
@ -144,9 +144,10 @@ It's also important to ensure that any background migrations have been fully com
|
||||||
before upgrading to a new major version. To see the current size of the `background_migration` queue,
|
before upgrading to a new major version. To see the current size of the `background_migration` queue,
|
||||||
[Check for background migrations before upgrading](../update/README.md#checking-for-background-migrations-before-upgrading).
|
[Check for background migrations before upgrading](../update/README.md#checking-for-background-migrations-before-upgrading).
|
||||||
|
|
||||||
To ensure background migrations are successful, increment by one minor version during the version jump before installing newer releases.
|
From version 12 onwards, an additional step is required. More significant migrations may occur during major release upgrades. To ensure these are successful, increment to the first minor version (`x.0.x`) during the major version jump. Then proceed with upgrading to a newer release.
|
||||||
|
|
||||||
|
For example: `11.11.x` -> `12.0.x` -> `12.8.x`
|
||||||
|
|
||||||
For example: `11.11.x` -> `12.0.x`
|
|
||||||
Please see the table below for some examples:
|
Please see the table below for some examples:
|
||||||
|
|
||||||
| Latest stable version | Your version | Recommended upgrade path | Note |
|
| Latest stable version | Your version | Recommended upgrade path | Note |
|
||||||
|
@ -154,7 +155,8 @@ Please see the table below for some examples:
|
||||||
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
|
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
|
||||||
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
|
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
|
||||||
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
|
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
|
||||||
| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.9` -> `12.5.8` | `11.11.8` is the last version in version `11` |
|
| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.5.8` | `11.11.8` is the last version in version `11`. `12.0.x` [is a required step.](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23211#note_272842444) |
|
||||||
|
| 12.8.5 | 9.2.6 | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.8.5` | Four intermediate versions required: the final 9.5, 10.8, 11.11 releases, plus 12.0 |
|
||||||
|
|
||||||
More information about the release procedures can be found in our
|
More information about the release procedures can be found in our
|
||||||
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
|
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
|
||||||
|
|
|
@ -40,6 +40,7 @@ stop_review:
|
||||||
environment:
|
environment:
|
||||||
name: review/$CI_COMMIT_REF_NAME
|
name: review/$CI_COMMIT_REF_NAME
|
||||||
action: stop
|
action: stop
|
||||||
|
dependencies: []
|
||||||
when: manual
|
when: manual
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
only:
|
only:
|
||||||
|
|
|
@ -17,9 +17,17 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def restore
|
def restore
|
||||||
@tree_hash = @group_hash || read_tree_hash
|
@relation_reader ||=
|
||||||
@group_members = @tree_hash.delete('members')
|
if @group_hash.present?
|
||||||
@children = @tree_hash.delete('children')
|
ImportExport::JSON::LegacyReader::User.new(@group_hash, reader.group_relation_names)
|
||||||
|
else
|
||||||
|
ImportExport::JSON::LegacyReader::File.new(@path, reader.group_relation_names)
|
||||||
|
end
|
||||||
|
|
||||||
|
@group_members = @relation_reader.consume_relation('members')
|
||||||
|
@children = @relation_reader.consume_attribute('children')
|
||||||
|
@relation_reader.consume_attribute('name')
|
||||||
|
@relation_reader.consume_attribute('path')
|
||||||
|
|
||||||
if members_mapper.map && restorer.restore
|
if members_mapper.map && restorer.restore
|
||||||
@children&.each do |group_hash|
|
@children&.each do |group_hash|
|
||||||
|
@ -45,21 +53,12 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def read_tree_hash
|
|
||||||
json = IO.read(@path)
|
|
||||||
ActiveSupport::JSON.decode(json)
|
|
||||||
rescue => e
|
|
||||||
@shared.error(e)
|
|
||||||
|
|
||||||
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
|
|
||||||
end
|
|
||||||
|
|
||||||
def restorer
|
def restorer
|
||||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||||
user: @user,
|
user: @user,
|
||||||
shared: @shared,
|
shared: @shared,
|
||||||
importable: @group,
|
importable: @group,
|
||||||
tree_hash: @tree_hash.except('name', 'path'),
|
relation_reader: @relation_reader,
|
||||||
members_mapper: members_mapper,
|
members_mapper: members_mapper,
|
||||||
object_builder: object_builder,
|
object_builder: object_builder,
|
||||||
relation_factory: relation_factory,
|
relation_factory: relation_factory,
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module ImportExport
|
||||||
|
module JSON
|
||||||
|
class LegacyReader
|
||||||
|
class File < LegacyReader
|
||||||
|
def initialize(path, relation_names)
|
||||||
|
@path = path
|
||||||
|
super(relation_names)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
::File.exist?(@path)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def tree_hash
|
||||||
|
@tree_hash ||= read_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_hash
|
||||||
|
ActiveSupport::JSON.decode(IO.read(@path))
|
||||||
|
rescue => e
|
||||||
|
Gitlab::ErrorTracking.log_exception(e)
|
||||||
|
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class User < LegacyReader
|
||||||
|
def initialize(tree_hash, relation_names)
|
||||||
|
@tree_hash = tree_hash
|
||||||
|
super(relation_names)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
@tree_hash.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_reader :tree_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(relation_names)
|
||||||
|
@relation_names = relation_names.map(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def legacy?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def root_attributes(excluded_attributes = [])
|
||||||
|
attributes.except(*excluded_attributes.map(&:to_s))
|
||||||
|
end
|
||||||
|
|
||||||
|
def consume_relation(key)
|
||||||
|
value = relations.delete(key)
|
||||||
|
|
||||||
|
return value unless block_given?
|
||||||
|
|
||||||
|
return if value.nil?
|
||||||
|
|
||||||
|
if value.is_a?(Array)
|
||||||
|
value.each.with_index do |item, idx|
|
||||||
|
yield(item, idx)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
yield(value, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def consume_attribute(key)
|
||||||
|
attributes.delete(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_ci_pipelines_by_id
|
||||||
|
relations['ci_pipelines']&.sort_by! { |hash| hash['id'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :relation_names
|
||||||
|
|
||||||
|
def tree_hash
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes
|
||||||
|
@attributes ||= tree_hash.slice!(*relation_names)
|
||||||
|
end
|
||||||
|
|
||||||
|
def relations
|
||||||
|
@relations ||= tree_hash.extract!(*relation_names)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,74 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Gitlab
|
|
||||||
module ImportExport
|
|
||||||
module Project
|
|
||||||
class TreeLoader
|
|
||||||
def load(path, dedup_entries: false)
|
|
||||||
tree_hash = ActiveSupport::JSON.decode(IO.read(path))
|
|
||||||
|
|
||||||
if dedup_entries
|
|
||||||
dedup_tree(tree_hash)
|
|
||||||
else
|
|
||||||
tree_hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# This function removes duplicate entries from the given tree recursively
|
|
||||||
# by caching nodes it encounters repeatedly. We only consider nodes for
|
|
||||||
# which there can actually be multiple equivalent instances (e.g. strings,
|
|
||||||
# hashes and arrays, but not `nil`s, numbers or booleans.)
|
|
||||||
#
|
|
||||||
# The algorithm uses a recursive depth-first descent with 3 cases, starting
|
|
||||||
# with a root node (the tree/hash itself):
|
|
||||||
# - a node has already been cached; in this case we return it from the cache
|
|
||||||
# - a node has not been cached yet but should be; descend into its children
|
|
||||||
# - a node is neither cached nor qualifies for caching; this is a no-op
|
|
||||||
def dedup_tree(node, nodes_seen = {})
|
|
||||||
if nodes_seen.key?(node) && distinguishable?(node)
|
|
||||||
yield nodes_seen[node]
|
|
||||||
elsif should_dedup?(node)
|
|
||||||
nodes_seen[node] = node
|
|
||||||
|
|
||||||
case node
|
|
||||||
when Array
|
|
||||||
node.each_index do |idx|
|
|
||||||
dedup_tree(node[idx], nodes_seen) do |cached_node|
|
|
||||||
node[idx] = cached_node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
when Hash
|
|
||||||
node.each do |k, v|
|
|
||||||
dedup_tree(v, nodes_seen) do |cached_node|
|
|
||||||
node[k] = cached_node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# We do not need to consider nodes for which there cannot be multiple instances
|
|
||||||
def should_dedup?(node)
|
|
||||||
node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
|
|
||||||
end
|
|
||||||
|
|
||||||
# We can only safely de-dup values that are distinguishable. True value objects
|
|
||||||
# are always distinguishable by nature. Hashes however can represent entities,
|
|
||||||
# which are identified by ID, not value. We therefore disallow de-duping hashes
|
|
||||||
# that do not have an `id` field, since we might risk dropping entities that
|
|
||||||
# have equal attributes yet different identities.
|
|
||||||
def distinguishable?(node)
|
|
||||||
if node.is_a?(Hash)
|
|
||||||
node.key?('id')
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -4,8 +4,6 @@ module Gitlab
|
||||||
module ImportExport
|
module ImportExport
|
||||||
module Project
|
module Project
|
||||||
class TreeRestorer
|
class TreeRestorer
|
||||||
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
|
|
||||||
|
|
||||||
attr_reader :user
|
attr_reader :user
|
||||||
attr_reader :shared
|
attr_reader :shared
|
||||||
attr_reader :project
|
attr_reader :project
|
||||||
|
@ -14,12 +12,12 @@ module Gitlab
|
||||||
@user = user
|
@user = user
|
||||||
@shared = shared
|
@shared = shared
|
||||||
@project = project
|
@project = project
|
||||||
@tree_loader = TreeLoader.new
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def restore
|
def restore
|
||||||
@tree_hash = read_tree_hash
|
@relation_reader = ImportExport::JSON::LegacyReader::File.new(File.join(shared.export_path, 'project.json'), reader.project_relation_names)
|
||||||
@project_members = @tree_hash.delete('project_members')
|
|
||||||
|
@project_members = @relation_reader.consume_relation('project_members')
|
||||||
|
|
||||||
if relation_tree_restorer.restore
|
if relation_tree_restorer.restore
|
||||||
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
|
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
|
||||||
|
@ -37,24 +35,12 @@ module Gitlab
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def large_project?(path)
|
|
||||||
File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_tree_hash
|
|
||||||
path = File.join(@shared.export_path, 'project.json')
|
|
||||||
@tree_loader.load(path, dedup_entries: large_project?(path))
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
|
|
||||||
raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
|
|
||||||
end
|
|
||||||
|
|
||||||
def relation_tree_restorer
|
def relation_tree_restorer
|
||||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||||
user: @user,
|
user: @user,
|
||||||
shared: @shared,
|
shared: @shared,
|
||||||
importable: @project,
|
importable: @project,
|
||||||
tree_hash: @tree_hash,
|
relation_reader: @relation_reader,
|
||||||
object_builder: object_builder,
|
object_builder: object_builder,
|
||||||
members_mapper: members_mapper,
|
members_mapper: members_mapper,
|
||||||
relation_factory: relation_factory,
|
relation_factory: relation_factory,
|
||||||
|
|
|
@ -17,10 +17,18 @@ module Gitlab
|
||||||
tree_by_key(:project)
|
tree_by_key(:project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def project_relation_names
|
||||||
|
attributes_finder.find_relations_tree(:project).keys
|
||||||
|
end
|
||||||
|
|
||||||
def group_tree
|
def group_tree
|
||||||
tree_by_key(:group)
|
tree_by_key(:group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_relation_names
|
||||||
|
attributes_finder.find_relations_tree(:group).keys
|
||||||
|
end
|
||||||
|
|
||||||
def group_members_tree
|
def group_members_tree
|
||||||
tree_by_key(:group_members)
|
tree_by_key(:group_members)
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,13 +9,13 @@ module Gitlab
|
||||||
attr_reader :user
|
attr_reader :user
|
||||||
attr_reader :shared
|
attr_reader :shared
|
||||||
attr_reader :importable
|
attr_reader :importable
|
||||||
attr_reader :tree_hash
|
attr_reader :relation_reader
|
||||||
|
|
||||||
def initialize(user:, shared:, importable:, tree_hash:, members_mapper:, object_builder:, relation_factory:, reader:)
|
def initialize(user:, shared:, importable:, relation_reader:, members_mapper:, object_builder:, relation_factory:, reader:)
|
||||||
@user = user
|
@user = user
|
||||||
@shared = shared
|
@shared = shared
|
||||||
@importable = importable
|
@importable = importable
|
||||||
@tree_hash = tree_hash
|
@relation_reader = relation_reader
|
||||||
@members_mapper = members_mapper
|
@members_mapper = members_mapper
|
||||||
@object_builder = object_builder
|
@object_builder = object_builder
|
||||||
@relation_factory = relation_factory
|
@relation_factory = relation_factory
|
||||||
|
@ -30,7 +30,7 @@ module Gitlab
|
||||||
bulk_inserts_enabled = @importable.class == ::Project &&
|
bulk_inserts_enabled = @importable.class == ::Project &&
|
||||||
Feature.enabled?(:import_bulk_inserts, @importable.group)
|
Feature.enabled?(:import_bulk_inserts, @importable.group)
|
||||||
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_inserts_enabled) do
|
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_inserts_enabled) do
|
||||||
update_relation_hashes!
|
fix_ci_pipelines_not_sorted_on_legacy_project_json!
|
||||||
create_relations!
|
create_relations!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,18 +57,8 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_relation!(relation_key, relation_definition)
|
def process_relation!(relation_key, relation_definition)
|
||||||
data_hashes = @tree_hash.delete(relation_key)
|
@relation_reader.consume_relation(relation_key) do |data_hash, relation_index|
|
||||||
return unless data_hashes
|
|
||||||
|
|
||||||
# we do not care if we process array or hash
|
|
||||||
data_hashes = [data_hashes] unless data_hashes.is_a?(Array)
|
|
||||||
|
|
||||||
relation_index = 0
|
|
||||||
|
|
||||||
# consume and remove objects from memory
|
|
||||||
while data_hash = data_hashes.shift
|
|
||||||
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
|
||||||
relation_index += 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -103,10 +93,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_params!
|
def update_params!
|
||||||
params = @tree_hash.reject do |key, _|
|
params = @relation_reader.root_attributes(relations.keys)
|
||||||
relations.include?(key)
|
|
||||||
end
|
|
||||||
|
|
||||||
params = params.merge(present_override_params)
|
params = params.merge(present_override_params)
|
||||||
|
|
||||||
# Cleaning all imported and overridden params
|
# Cleaning all imported and overridden params
|
||||||
|
@ -223,8 +210,13 @@ module Gitlab
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_relation_hashes!
|
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
|
||||||
@tree_hash['ci_pipelines']&.sort_by! { |hash| hash['id'] }
|
# This should be removed once legacy JSON format is deprecated.
|
||||||
|
# Ndjson export file will fix the order during project export.
|
||||||
|
def fix_ci_pipelines_not_sorted_on_legacy_project_json!
|
||||||
|
return unless relation_reader.legacy?
|
||||||
|
|
||||||
|
relation_reader.sort_ci_pipelines_by_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ module Gitlab
|
||||||
def save(tree, dir_path, filename)
|
def save(tree, dir_path, filename)
|
||||||
mkdir_p(dir_path)
|
mkdir_p(dir_path)
|
||||||
|
|
||||||
tree_json = JSON.generate(tree)
|
tree_json = ::JSON.generate(tree)
|
||||||
|
|
||||||
File.write(File.join(dir_path, filename), tree_json)
|
File.write(File.join(dir_path, filename), tree_json)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"invalid" json
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
/* global Mousetrap */
|
||||||
|
// `mousetrap` uses amd which webpack understands but Jest does not
|
||||||
|
// Thankfully it also writes to a global export so we can es6-ify it
|
||||||
|
import 'mousetrap';
|
||||||
|
|
||||||
|
export default Mousetrap;
|
|
@ -1,6 +1,7 @@
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import { GlLoadingIcon } from '@gitlab/ui';
|
import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import App from '~/diffs/components/app.vue';
|
import App from '~/diffs/components/app.vue';
|
||||||
|
@ -12,14 +13,17 @@ import CommitWidget from '~/diffs/components/commit_widget.vue';
|
||||||
import TreeList from '~/diffs/components/tree_list.vue';
|
import TreeList from '~/diffs/components/tree_list.vue';
|
||||||
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
|
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
|
||||||
import createDiffsStore from '../create_diffs_store';
|
import createDiffsStore from '../create_diffs_store';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import diffsMockData from '../mock_data/merge_request_diffs';
|
import diffsMockData from '../mock_data/merge_request_diffs';
|
||||||
|
|
||||||
const mergeRequestDiff = { version_index: 1 };
|
const mergeRequestDiff = { version_index: 1 };
|
||||||
|
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
|
||||||
|
|
||||||
describe('diffs/components/app', () => {
|
describe('diffs/components/app', () => {
|
||||||
const oldMrTabs = window.mrTabs;
|
const oldMrTabs = window.mrTabs;
|
||||||
let store;
|
let store;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let mock;
|
||||||
|
|
||||||
function createComponent(props = {}, extendStore = () => {}) {
|
function createComponent(props = {}, extendStore = () => {}) {
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
|
@ -34,7 +38,7 @@ describe('diffs/components/app', () => {
|
||||||
wrapper = shallowMount(localVue.extend(App), {
|
wrapper = shallowMount(localVue.extend(App), {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
endpoint: `${TEST_HOST}/diff/endpoint`,
|
endpoint: TEST_ENDPOINT,
|
||||||
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
|
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
|
||||||
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
|
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
|
||||||
projectPath: 'namespace/project',
|
projectPath: 'namespace/project',
|
||||||
|
@ -61,8 +65,12 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// setup globals (needed for component to mount :/)
|
// setup globals (needed for component to mount :/)
|
||||||
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
|
window.mrTabs = {
|
||||||
window.mrTabs.expandViewContainer = jasmine.createSpy();
|
resetViewContainer: jest.fn(),
|
||||||
|
};
|
||||||
|
window.mrTabs.expandViewContainer = jest.fn();
|
||||||
|
mock = new MockAdapter(axios);
|
||||||
|
mock.onGet(TEST_ENDPOINT).reply(200, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -71,6 +79,8 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
// reset component
|
// reset component
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetch diff methods', () => {
|
describe('fetch diff methods', () => {
|
||||||
|
@ -80,15 +90,15 @@ describe('diffs/components/app', () => {
|
||||||
store.state.notes.discussions = 'test';
|
store.state.notes.discussions = 'test';
|
||||||
return Promise.resolve({ real_size: 100 });
|
return Promise.resolve({ real_size: 100 });
|
||||||
};
|
};
|
||||||
spyOn(window, 'requestIdleCallback').and.callFake(fn => fn());
|
jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
|
||||||
createComponent();
|
createComponent();
|
||||||
spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(fetchResolver);
|
jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver);
|
||||||
spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(fetchResolver);
|
jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver);
|
||||||
spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(fetchResolver);
|
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
|
||||||
spyOn(wrapper.vm, 'setDiscussions');
|
jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
|
||||||
spyOn(wrapper.vm, 'startRenderDiffsQueue');
|
jest.spyOn(wrapper.vm, 'startRenderDiffsQueue').mockImplementation(() => {});
|
||||||
spyOn(wrapper.vm, 'unwatchDiscussions');
|
jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
|
||||||
spyOn(wrapper.vm, 'unwatchRetrievingBatches');
|
jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
|
||||||
store.state.diffs.retrievingBatches = true;
|
store.state.diffs.retrievingBatches = true;
|
||||||
store.state.diffs.diffFiles = [];
|
store.state.diffs.diffFiles = [];
|
||||||
wrapper.vm.$nextTick(done);
|
wrapper.vm.$nextTick(done);
|
||||||
|
@ -236,7 +246,7 @@ describe('diffs/components/app', () => {
|
||||||
wrapper.vm.fetchData(false);
|
wrapper.vm.fetchData(false);
|
||||||
|
|
||||||
expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled();
|
||||||
setTimeout(() => {
|
setImmediate(() => {
|
||||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
|
||||||
|
@ -255,7 +265,7 @@ describe('diffs/components/app', () => {
|
||||||
wrapper.vm.fetchData(false);
|
wrapper.vm.fetchData(false);
|
||||||
|
|
||||||
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
||||||
setTimeout(() => {
|
setImmediate(() => {
|
||||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
||||||
|
@ -272,7 +282,7 @@ describe('diffs/components/app', () => {
|
||||||
wrapper.vm.fetchData(false);
|
wrapper.vm.fetchData(false);
|
||||||
|
|
||||||
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
||||||
setTimeout(() => {
|
setImmediate(() => {
|
||||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
||||||
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
||||||
|
@ -350,23 +360,21 @@ describe('diffs/components/app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Component uses $nextTick so we wait until that has finished
|
// Component uses $nextTick so we wait until that has finished
|
||||||
setTimeout(() => {
|
setImmediate(() => {
|
||||||
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
|
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks current diff file based on currently highlighted row', done => {
|
it('marks current diff file based on currently highlighted row', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
shouldShow: true,
|
shouldShow: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Component uses $nextTick so we wait until that has finished
|
// Component uses $nextTick so we wait until that has finished
|
||||||
setTimeout(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
|
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -403,7 +411,7 @@ describe('diffs/components/app', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Component uses $nextTick so we wait until that has finished
|
// Component uses $nextTick so we wait until that has finished
|
||||||
setTimeout(() => {
|
setImmediate(() => {
|
||||||
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
|
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
@ -449,7 +457,7 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
describe('visible app', () => {
|
describe('visible app', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spy = jasmine.createSpy('spy');
|
spy = jest.fn();
|
||||||
|
|
||||||
createComponent({
|
createComponent({
|
||||||
shouldShow: true,
|
shouldShow: true,
|
||||||
|
@ -459,21 +467,18 @@ describe('diffs/components/app', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => {
|
it.each(Object.keys(mappings))(
|
||||||
wrapper.vm
|
'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed',
|
||||||
.$nextTick()
|
key => {
|
||||||
.then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
Object.keys(mappings).forEach(function(key) {
|
expect(spy).not.toHaveBeenCalled();
|
||||||
Mousetrap.trigger(key);
|
|
||||||
|
|
||||||
expect(spy.calls.mostRecent().args).toEqual([mappings[key]]);
|
Mousetrap.trigger(key);
|
||||||
});
|
|
||||||
|
|
||||||
expect(spy.calls.count()).toEqual(Object.keys(mappings).length);
|
expect(spy).toHaveBeenCalledWith(mappings[key]);
|
||||||
})
|
});
|
||||||
.then(done)
|
},
|
||||||
.catch(done.fail);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call `jumpToFile()` when unknown key is pressed', done => {
|
it('does not call `jumpToFile()` when unknown key is pressed', done => {
|
||||||
wrapper.vm
|
wrapper.vm
|
||||||
|
@ -490,7 +495,7 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
describe('hideen app', () => {
|
describe('hideen app', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spy = jasmine.createSpy('spy');
|
spy = jest.fn();
|
||||||
|
|
||||||
createComponent({
|
createComponent({
|
||||||
shouldShow: false,
|
shouldShow: false,
|
||||||
|
@ -504,7 +509,7 @@ describe('diffs/components/app', () => {
|
||||||
wrapper.vm
|
wrapper.vm
|
||||||
.$nextTick()
|
.$nextTick()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Object.keys(mappings).forEach(function(key) {
|
Object.keys(mappings).forEach(key => {
|
||||||
Mousetrap.trigger(key);
|
Mousetrap.trigger(key);
|
||||||
|
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
|
@ -520,7 +525,7 @@ describe('diffs/components/app', () => {
|
||||||
let spy;
|
let spy;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spy = jasmine.createSpy();
|
spy = jest.fn();
|
||||||
|
|
||||||
createComponent({}, () => {
|
createComponent({}, () => {
|
||||||
store.state.diffs.diffFiles = [
|
store.state.diffs.diffFiles = [
|
||||||
|
@ -545,15 +550,15 @@ describe('diffs/components/app', () => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
wrapper.vm.jumpToFile(+1);
|
wrapper.vm.jumpToFile(+1);
|
||||||
|
|
||||||
expect(spy.calls.mostRecent().args).toEqual(['222.js']);
|
expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']);
|
||||||
store.state.diffs.currentDiffFileId = '222';
|
store.state.diffs.currentDiffFileId = '222';
|
||||||
wrapper.vm.jumpToFile(+1);
|
wrapper.vm.jumpToFile(+1);
|
||||||
|
|
||||||
expect(spy.calls.mostRecent().args).toEqual(['333.js']);
|
expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['333.js']);
|
||||||
store.state.diffs.currentDiffFileId = '333';
|
store.state.diffs.currentDiffFileId = '333';
|
||||||
wrapper.vm.jumpToFile(-1);
|
wrapper.vm.jumpToFile(-1);
|
||||||
|
|
||||||
expect(spy.calls.mostRecent().args).toEqual(['222.js']);
|
expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']);
|
||||||
})
|
})
|
||||||
.then(done)
|
.then(done)
|
||||||
.catch(done.fail);
|
.catch(done.fail);
|
||||||
|
@ -602,7 +607,7 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
expect(wrapper.contains(CompareVersions)).toBe(true);
|
expect(wrapper.contains(CompareVersions)).toBe(true);
|
||||||
expect(wrapper.find(CompareVersions).props()).toEqual(
|
expect(wrapper.find(CompareVersions).props()).toEqual(
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
targetBranch: {
|
targetBranch: {
|
||||||
branchName: 'target-branch',
|
branchName: 'target-branch',
|
||||||
versionIndex: -1,
|
versionIndex: -1,
|
||||||
|
@ -625,7 +630,7 @@ describe('diffs/components/app', () => {
|
||||||
|
|
||||||
expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
|
expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
|
||||||
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
|
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
|
||||||
jasmine.objectContaining({
|
expect.objectContaining({
|
||||||
total: '5',
|
total: '5',
|
||||||
plainDiffPath: 'plain diff path',
|
plainDiffPath: 'plain diff path',
|
||||||
emailPatchPath: 'email patch path',
|
emailPatchPath: 'email patch path',
|
||||||
|
@ -663,7 +668,7 @@ describe('diffs/components/app', () => {
|
||||||
let toggleShowTreeList;
|
let toggleShowTreeList;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
toggleShowTreeList = jasmine.createSpy('toggleShowTreeList');
|
toggleShowTreeList = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import diffsModule from '~/diffs/store/modules';
|
||||||
|
import notesModule from '~/notes/stores/modules';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
export default function createDiffsStore() {
|
||||||
|
return new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
diffs: diffsModule(),
|
||||||
|
notes: notesModule(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
|
||||||
|
|
||||||
<empty-state-stub
|
<empty-state-stub
|
||||||
cansetci="true"
|
cansetci="true"
|
||||||
|
class="mb-auto mt-auto"
|
||||||
emptystatesvgpath="http://test.host"
|
emptystatesvgpath="http://test.host"
|
||||||
helppagepath="http://test.host"
|
helppagepath="http://test.host"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import * as iconUtils from '~/lib/utils/icon_utils';
|
import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils';
|
||||||
|
|
||||||
describe('Icon utils', () => {
|
describe('Icon utils', () => {
|
||||||
describe('getSvgIconPathContent', () => {
|
describe('getSvgIconPathContent', () => {
|
||||||
let spriteIcons;
|
let spriteIcons;
|
||||||
|
let axiosMock;
|
||||||
|
const mockName = 'mockIconName';
|
||||||
|
const mockPath = 'mockPath';
|
||||||
|
const mockIcons = `<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
spriteIcons = gon.sprite_icons;
|
spriteIcons = gon.sprite_icons;
|
||||||
|
@ -15,45 +19,63 @@ describe('Icon utils', () => {
|
||||||
gon.sprite_icons = spriteIcons;
|
gon.sprite_icons = spriteIcons;
|
||||||
});
|
});
|
||||||
|
|
||||||
let axiosMock;
|
|
||||||
let mockEndpoint;
|
|
||||||
const mockName = 'mockIconName';
|
|
||||||
const mockPath = 'mockPath';
|
|
||||||
const getIcon = () => iconUtils.getSvgIconPathContent(mockName);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock = new MockAdapter(axios);
|
axiosMock = new MockAdapter(axios);
|
||||||
mockEndpoint = axiosMock.onGet(gon.sprite_icons);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
axiosMock.restore();
|
axiosMock.restore();
|
||||||
|
clearSvgIconPathContentCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('extracts svg icon path content from sprite icons', () => {
|
describe('when the icons can be loaded', () => {
|
||||||
mockEndpoint.replyOnce(
|
beforeEach(() => {
|
||||||
200,
|
axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons);
|
||||||
`<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return getIcon().then(path => {
|
it('extracts svg icon path content from sprite icons', () => {
|
||||||
expect(path).toBe(mockPath);
|
return getSvgIconPathContent(mockName).then(path => {
|
||||||
|
expect(path).toBe(mockPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null if icon path content does not exist', () => {
|
||||||
|
return getSvgIconPathContent('missing-icon').then(path => {
|
||||||
|
expect(path).toBe(null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null if icon path content does not exist', () => {
|
describe('when the icons cannot be loaded on the first 2 tries', () => {
|
||||||
mockEndpoint.replyOnce(200, ``);
|
beforeEach(() => {
|
||||||
|
axiosMock
|
||||||
return getIcon().then(path => {
|
.onGet(gon.sprite_icons)
|
||||||
expect(path).toBe(null);
|
.replyOnce(500)
|
||||||
|
.onGet(gon.sprite_icons)
|
||||||
|
.replyOnce(500)
|
||||||
|
.onGet(gon.sprite_icons)
|
||||||
|
.reply(200, mockIcons);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('returns null if an http error occurs', () => {
|
it('returns null', () => {
|
||||||
mockEndpoint.replyOnce(500);
|
return getSvgIconPathContent(mockName).then(path => {
|
||||||
|
expect(path).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return getIcon().then(path => {
|
it('extracts svg icon path content, after 2 attempts', () => {
|
||||||
expect(path).toBe(null);
|
return getSvgIconPathContent(mockName)
|
||||||
|
.then(path1 => {
|
||||||
|
expect(path1).toBe(null);
|
||||||
|
return getSvgIconPathContent(mockName);
|
||||||
|
})
|
||||||
|
.then(path2 => {
|
||||||
|
expect(path2).toBe(null);
|
||||||
|
return getSvgIconPathContent(mockName);
|
||||||
|
})
|
||||||
|
.then(path3 => {
|
||||||
|
expect(path3).toBe(mockPath);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
export default class TreeWorkerMock {
|
||||||
|
addEventListener() {}
|
||||||
|
|
||||||
|
terminate() {}
|
||||||
|
|
||||||
|
postMessage() {}
|
||||||
|
}
|
|
@ -1,15 +1 @@
|
||||||
import Vue from 'vue';
|
export { default } from '../../frontend/diffs/create_diffs_store';
|
||||||
import Vuex from 'vuex';
|
|
||||||
import diffsModule from '~/diffs/store/modules';
|
|
||||||
import notesModule from '~/notes/stores/modules';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
export default function createDiffsStore() {
|
|
||||||
return new Vuex.Store({
|
|
||||||
modules: {
|
|
||||||
diffs: diffsModule(),
|
|
||||||
notes: notesModule(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Gitlab::ImportExport::JSON::LegacyReader::User do
|
||||||
|
let(:relation_names) { [] }
|
||||||
|
let(:legacy_reader) { described_class.new(tree_hash, relation_names) }
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
subject { legacy_reader.valid? }
|
||||||
|
|
||||||
|
context 'tree_hash not present' do
|
||||||
|
let(:tree_hash) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'tree_hash presents' do
|
||||||
|
let(:tree_hash) { { "issues": [] } }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Gitlab::ImportExport::JSON::LegacyReader::File do
|
||||||
|
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
|
||||||
|
let(:project_tree) { JSON.parse(File.read(fixture)) }
|
||||||
|
let(:relation_names) { [] }
|
||||||
|
let(:legacy_reader) { described_class.new(path, relation_names) }
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
subject { legacy_reader.valid? }
|
||||||
|
|
||||||
|
context 'given valid path' do
|
||||||
|
let(:path) { fixture }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'given invalid path' do
|
||||||
|
let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#root_attributes' do
|
||||||
|
let(:path) { fixture }
|
||||||
|
|
||||||
|
subject { legacy_reader.root_attributes(excluded_attributes) }
|
||||||
|
|
||||||
|
context 'No excluded attributes' do
|
||||||
|
let(:excluded_attributes) { [] }
|
||||||
|
let(:relation_names) { [] }
|
||||||
|
|
||||||
|
it 'returns the whole tree from parsed JSON' do
|
||||||
|
expect(subject).to eq(project_tree)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'Some attributes are excluded' do
|
||||||
|
let(:excluded_attributes) { %w[milestones labels issues services snippets] }
|
||||||
|
let(:relation_names) { %w[import_type archived] }
|
||||||
|
|
||||||
|
it 'returns hash without excluded attributes and relations' do
|
||||||
|
expect(subject).not_to include('milestones', 'labels', 'issues', 'services', 'snippets', 'import_type', 'archived')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#consume_relation' do
|
||||||
|
let(:path) { fixture }
|
||||||
|
let(:key) { 'description' }
|
||||||
|
|
||||||
|
context 'block not given' do
|
||||||
|
it 'returns value of the key' do
|
||||||
|
expect(legacy_reader).to receive(:relations).and_return({ key => 'test value' })
|
||||||
|
expect(legacy_reader.consume_relation(key)).to eq('test value')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'key has been consumed' do
|
||||||
|
before do
|
||||||
|
legacy_reader.consume_relation(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not yield' do
|
||||||
|
expect do |blk|
|
||||||
|
legacy_reader.consume_relation(key, &blk)
|
||||||
|
end.not_to yield_control
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'value is nil' do
|
||||||
|
before do
|
||||||
|
expect(legacy_reader).to receive(:relations).and_return({ key => nil })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not yield' do
|
||||||
|
expect do |blk|
|
||||||
|
legacy_reader.consume_relation(key, &blk)
|
||||||
|
end.not_to yield_control
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'value is not array' do
|
||||||
|
before do
|
||||||
|
expect(legacy_reader).to receive(:relations).and_return({ key => 'value' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'yield the value with index 0' do
|
||||||
|
expect do |blk|
|
||||||
|
legacy_reader.consume_relation(key, &blk)
|
||||||
|
end.to yield_with_args('value', 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'value is an array' do
|
||||||
|
before do
|
||||||
|
expect(legacy_reader).to receive(:relations).and_return({ key => %w[item1 item2 item3] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'yield each array element with index' do
|
||||||
|
expect do |blk|
|
||||||
|
legacy_reader.consume_relation(key, &blk)
|
||||||
|
end.to yield_successive_args(['item1', 0], ['item2', 1], ['item3', 2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tree_hash' do
|
||||||
|
let(:path) { fixture }
|
||||||
|
|
||||||
|
subject { legacy_reader.send(:tree_hash) }
|
||||||
|
|
||||||
|
it 'parses the JSON into the expected tree' do
|
||||||
|
expect(subject).to eq(project_tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid JSON' do
|
||||||
|
let(:path) { 'spec/fixtures/lib/gitlab/import_export/invalid_json/project.json' }
|
||||||
|
|
||||||
|
it 'raise Exception' do
|
||||||
|
expect { subject }.to raise_exception(Gitlab::ImportExport::Error, 'Incorrect JSON format')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,49 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
describe Gitlab::ImportExport::Project::TreeLoader do
|
|
||||||
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
|
|
||||||
let(:project_tree) { JSON.parse(File.read(fixture)) }
|
|
||||||
|
|
||||||
context 'without de-duplicating entries' do
|
|
||||||
let(:parsed_tree) do
|
|
||||||
subject.load(fixture)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'parses the JSON into the expected tree' do
|
|
||||||
expect(parsed_tree).to eq(project_tree)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not de-duplicate entries' do
|
|
||||||
expect(parsed_tree['duped_hash_with_id']).not_to be(parsed_tree['array'][0]['duped_hash_with_id'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with de-duplicating entries' do
|
|
||||||
let(:parsed_tree) do
|
|
||||||
subject.load(fixture, dedup_entries: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'parses the JSON into the expected tree' do
|
|
||||||
expect(parsed_tree).to eq(project_tree)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'de-duplicates equal values' do
|
|
||||||
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['array'][0]['duped_hash_with_id'])
|
|
||||||
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['nested']['duped_hash_with_id'])
|
|
||||||
expect(parsed_tree['duped_array']).to be(parsed_tree['array'][1]['duped_array'])
|
|
||||||
expect(parsed_tree['duped_array']).to be(parsed_tree['nested']['duped_array'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not de-duplicate hashes without IDs' do
|
|
||||||
expect(parsed_tree['duped_hash_no_id']).to eq(parsed_tree['array'][2]['duped_hash_no_id'])
|
|
||||||
expect(parsed_tree['duped_hash_no_id']).not_to be(parsed_tree['array'][2]['duped_hash_no_id'])
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'keeps single entries intact' do
|
|
||||||
expect(parsed_tree['simple']).to eq(42)
|
|
||||||
expect(parsed_tree['nested']['array']).to eq(["don't touch"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -783,7 +783,8 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
expect(restorer).to receive(:read_tree_hash) { tree_hash }
|
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:valid?).and_return(true)
|
||||||
|
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:tree_hash) { tree_hash }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'no group visibility' do
|
context 'no group visibility' do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This spec is a lightweight version of:
|
# This spec is a lightweight version of:
|
||||||
# * project_tree_restorer_spec.rb
|
# * project/tree_restorer_spec.rb
|
||||||
#
|
#
|
||||||
# In depth testing is being done in the above specs.
|
# In depth testing is being done in the above specs.
|
||||||
# This spec tests that restore project works
|
# This spec tests that restore project works
|
||||||
|
@ -25,7 +25,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
||||||
described_class.new(
|
described_class.new(
|
||||||
user: user,
|
user: user,
|
||||||
shared: shared,
|
shared: shared,
|
||||||
tree_hash: tree_hash,
|
relation_reader: relation_reader,
|
||||||
importable: importable,
|
importable: importable,
|
||||||
object_builder: object_builder,
|
object_builder: object_builder,
|
||||||
members_mapper: members_mapper,
|
members_mapper: members_mapper,
|
||||||
|
@ -36,14 +36,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
||||||
|
|
||||||
subject { relation_tree_restorer.restore }
|
subject { relation_tree_restorer.restore }
|
||||||
|
|
||||||
context 'when restoring a project' do
|
shared_examples 'import project successfully' do
|
||||||
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
|
|
||||||
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
|
|
||||||
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
|
|
||||||
let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
|
|
||||||
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
|
|
||||||
let(:tree_hash) { importable_hash }
|
|
||||||
|
|
||||||
it 'restores project tree' do
|
it 'restores project tree' do
|
||||||
expect(subject).to eq(true)
|
expect(subject).to eq(true)
|
||||||
end
|
end
|
||||||
|
@ -66,4 +59,18 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when restoring a project' do
|
||||||
|
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
|
||||||
|
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
|
||||||
|
let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
|
||||||
|
let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
|
||||||
|
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
|
||||||
|
|
||||||
|
context 'using legacy reader' do
|
||||||
|
let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, reader.project_relation_names) }
|
||||||
|
|
||||||
|
it_behaves_like 'import project successfully'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -625,4 +625,14 @@ describe SystemNoteService do
|
||||||
described_class.discussion_lock(issuable, double)
|
described_class.discussion_lock(issuable, double)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.auto_resolve_prometheus_alert' do
|
||||||
|
it 'calls IssuableService' do
|
||||||
|
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
|
||||||
|
expect(service).to receive(:auto_resolve_prometheus_alert)
|
||||||
|
end
|
||||||
|
|
||||||
|
described_class.auto_resolve_prometheus_alert(noteable, project, author)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -654,4 +654,16 @@ describe ::SystemNotes::IssuablesService do
|
||||||
.to eq('resolved the corresponding error and closed the issue.')
|
.to eq('resolved the corresponding error and closed the issue.')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#auto_resolve_prometheus_alert' do
|
||||||
|
subject { service.auto_resolve_prometheus_alert }
|
||||||
|
|
||||||
|
it_behaves_like 'a system note' do
|
||||||
|
let(:action) { 'closed' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the expected system note' do
|
||||||
|
expect(subject.note).to eq('automatically closed this issue because the alert resolved.')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue