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/.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
|
||||
StylesPath = doc/.vale
|
||||
MinAlertLevel = suggestion
|
||||
|
||||
# Should Vale parse any file formats other than .md files as Markdown?
|
||||
# --------------------------------------------------------------------
|
||||
[formats]
|
||||
mdx = md
|
||||
|
||||
# What file types should Vale test?
|
||||
# ----------------------------------
|
||||
[*.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
|
||||
|
||||
# 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 { joinPaths } from './lib/utils/url_utility';
|
||||
import flash from '~/flash';
|
||||
|
@ -70,7 +68,7 @@ const Api = {
|
|||
},
|
||||
|
||||
// Return groups list. Filtered by query
|
||||
groups(query, options, callback = $.noop) {
|
||||
groups(query, options, callback = () => {}) {
|
||||
const url = Api.buildUrl(Api.groupsPath);
|
||||
return axios
|
||||
.get(url, {
|
||||
|
@ -108,7 +106,7 @@ const Api = {
|
|||
},
|
||||
|
||||
// Return projects list. Filtered by query
|
||||
projects(query, options, callback = _.noop) {
|
||||
projects(query, options, callback = () => {}) {
|
||||
const url = Api.buildUrl(Api.projectsPath);
|
||||
const defaults = {
|
||||
search: query,
|
||||
|
|
|
@ -26,15 +26,17 @@ export default {
|
|||
modalInfo: {
|
||||
closeText: s__('EnableReviewApp|Close'),
|
||||
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
|
||||
copyString: `deploy_review
|
||||
copyString: `deploy_review:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "Deploy a review app"
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
||||
only: branches
|
||||
except: master`,
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master`,
|
||||
id: 'enable-review-app-info',
|
||||
title: s__('ReviewApp|Enable Review App'),
|
||||
},
|
||||
|
|
|
@ -94,7 +94,7 @@ export default {
|
|||
data-boundary="viewport"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,7 @@ export default {
|
|||
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
|
||||
<template v-else-if="hasLoadedPipeline">
|
||||
<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">
|
||||
<strong> {{ __('Pipeline') }} </strong>
|
||||
<a
|
||||
|
@ -76,6 +76,7 @@ export default {
|
|||
:help-page-path="links.ciHelpPagePath"
|
||||
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
|
||||
:can-set-ci="true"
|
||||
class="mb-auto mt-auto"
|
||||
/>
|
||||
<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>
|
||||
|
|
|
@ -1,18 +1,40 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import { memoize } from 'lodash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
/**
|
||||
* Retrieve SVG icon path content from gitlab/svg sprite icons
|
||||
* @param {String} name
|
||||
* Resolves to a DOM that contains GitLab icons
|
||||
* in svg format. Memoized to avoid duplicate requests
|
||||
*/
|
||||
export const getSvgIconPathContent = name =>
|
||||
const getSvgDom = memoize(() =>
|
||||
axios
|
||||
.get(gon.sprite_icons)
|
||||
.then(({ data: svgs }) =>
|
||||
new DOMParser()
|
||||
.parseFromString(svgs, 'text/xml')
|
||||
.querySelector(`#${name} path`)
|
||||
.getAttribute('d'),
|
||||
)
|
||||
.then(({ data: svgs }) => new DOMParser().parseFromString(svgs, 'text/xml'))
|
||||
.catch(() => {
|
||||
getSvgDom.cache.clear();
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
|
|
@ -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/mixins';
|
||||
@import './ide_mixins';
|
||||
@import './ide_monaco_overrides';
|
||||
|
||||
$search-list-icon-width: 18px;
|
||||
$ide-activity-bar-width: 60px;
|
||||
|
@ -16,11 +17,6 @@ $ide-commit-header-height: 48px;
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.commit-message {
|
||||
@include str-truncated(250px);
|
||||
}
|
||||
|
@ -49,10 +45,6 @@ $ide-commit-header-height: 48px;
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0; // firefox fix
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-loading-container {
|
||||
|
@ -160,157 +152,6 @@ $ide-commit-header-height: 48px;
|
|||
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 {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
|
@ -671,10 +512,6 @@ $ide-commit-header-height: 48px;
|
|||
width: $ide-commit-row-height;
|
||||
height: $ide-commit-row-height;
|
||||
color: inherit;
|
||||
|
||||
> svg {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-commit-file-count {
|
||||
|
@ -864,39 +701,39 @@ $ide-commit-header-height: 48px;
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ide-nav-dropdown {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
button {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
width: 385px;
|
||||
max-height: initial;
|
||||
}
|
||||
.ide-nav-dropdown {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
color: $gray-700;
|
||||
.dropdown-menu {
|
||||
width: 385px;
|
||||
max-height: initial;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
.dropdown-menu-toggle {
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
color: $gray-700;
|
||||
|
||||
&:hover {
|
||||
background-color: $white-normal;
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
.dropdown-menu-toggle {
|
||||
background-color: $white-dark;
|
||||
}
|
||||
&:hover {
|
||||
background-color: $white-normal;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
color: $gl-text-color;
|
||||
&.show {
|
||||
.dropdown-menu-toggle {
|
||||
background-color: $white-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -945,6 +782,8 @@ $ide-commit-header-height: 48px;
|
|||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to,
|
||||
.commit-form-slide-up-enter,
|
||||
.commit-form-slide-up-leave-to {
|
||||
opacity: 0;
|
||||
|
@ -1063,9 +902,6 @@ $ide-commit-header-height: 48px;
|
|||
@include ide-trace-view();
|
||||
|
||||
.empty-state {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
p {
|
||||
margin: $grid-size 0;
|
||||
text-align: center;
|
||||
|
@ -1092,10 +928,6 @@ $ide-commit-header-height: 48px;
|
|||
min-height: 55px;
|
||||
padding-left: $gl-padding;
|
||||
padding-right: $gl-padding;
|
||||
|
||||
.ci-status-icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-job-item {
|
||||
|
@ -1135,7 +967,7 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
|
||||
.ide-nav-form {
|
||||
.nav-links li {
|
||||
li {
|
||||
width: 50%;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
@ -1222,10 +1054,6 @@ $ide-commit-header-height: 48px;
|
|||
background-color: $blue-500;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-new-btn {
|
||||
|
|
|
@ -241,6 +241,10 @@ module SystemNoteService
|
|||
def zoom_link_removed(issue, project, author)
|
||||
::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_removed
|
||||
end
|
||||
|
||||
def auto_resolve_prometheus_alert(noteable, project, author)
|
||||
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).auto_resolve_prometheus_alert
|
||||
end
|
||||
end
|
||||
|
||||
SystemNoteService.prepend_if_ee('EE::SystemNoteService')
|
||||
|
|
|
@ -288,6 +288,12 @@ module SystemNotes
|
|||
create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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`
|
||||
- `10.6.3` -> `10.6.6`
|
||||
- `11.11.1` -> `11.11.8`
|
||||
- `12.0.4` -> `12.0.9`
|
||||
- `12.0.4` -> `12.0.12`
|
||||
- Upgrade the minor version:
|
||||
- `8.9.4` -> `8.12.3`
|
||||
- `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,
|
||||
[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:
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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
|
||||
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
|
||||
|
|
|
@ -40,6 +40,7 @@ stop_review:
|
|||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
action: stop
|
||||
dependencies: []
|
||||
when: manual
|
||||
allow_failure: true
|
||||
only:
|
||||
|
|
|
@ -17,9 +17,17 @@ module Gitlab
|
|||
end
|
||||
|
||||
def restore
|
||||
@tree_hash = @group_hash || read_tree_hash
|
||||
@group_members = @tree_hash.delete('members')
|
||||
@children = @tree_hash.delete('children')
|
||||
@relation_reader ||=
|
||||
if @group_hash.present?
|
||||
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
|
||||
@children&.each do |group_hash|
|
||||
|
@ -45,21 +53,12 @@ module Gitlab
|
|||
|
||||
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
|
||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||
user: @user,
|
||||
shared: @shared,
|
||||
importable: @group,
|
||||
tree_hash: @tree_hash.except('name', 'path'),
|
||||
relation_reader: @relation_reader,
|
||||
members_mapper: members_mapper,
|
||||
object_builder: object_builder,
|
||||
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 Project
|
||||
class TreeRestorer
|
||||
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :shared
|
||||
attr_reader :project
|
||||
|
@ -14,12 +12,12 @@ module Gitlab
|
|||
@user = user
|
||||
@shared = shared
|
||||
@project = project
|
||||
@tree_loader = TreeLoader.new
|
||||
end
|
||||
|
||||
def restore
|
||||
@tree_hash = read_tree_hash
|
||||
@project_members = @tree_hash.delete('project_members')
|
||||
@relation_reader = ImportExport::JSON::LegacyReader::File.new(File.join(shared.export_path, 'project.json'), reader.project_relation_names)
|
||||
|
||||
@project_members = @relation_reader.consume_relation('project_members')
|
||||
|
||||
if relation_tree_restorer.restore
|
||||
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
|
||||
|
@ -37,24 +35,12 @@ module Gitlab
|
|||
|
||||
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
|
||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||
user: @user,
|
||||
shared: @shared,
|
||||
importable: @project,
|
||||
tree_hash: @tree_hash,
|
||||
relation_reader: @relation_reader,
|
||||
object_builder: object_builder,
|
||||
members_mapper: members_mapper,
|
||||
relation_factory: relation_factory,
|
||||
|
|
|
@ -17,10 +17,18 @@ module Gitlab
|
|||
tree_by_key(:project)
|
||||
end
|
||||
|
||||
def project_relation_names
|
||||
attributes_finder.find_relations_tree(:project).keys
|
||||
end
|
||||
|
||||
def group_tree
|
||||
tree_by_key(:group)
|
||||
end
|
||||
|
||||
def group_relation_names
|
||||
attributes_finder.find_relations_tree(:group).keys
|
||||
end
|
||||
|
||||
def group_members_tree
|
||||
tree_by_key(:group_members)
|
||||
end
|
||||
|
|
|
@ -9,13 +9,13 @@ module Gitlab
|
|||
attr_reader :user
|
||||
attr_reader :shared
|
||||
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
|
||||
@shared = shared
|
||||
@importable = importable
|
||||
@tree_hash = tree_hash
|
||||
@relation_reader = relation_reader
|
||||
@members_mapper = members_mapper
|
||||
@object_builder = object_builder
|
||||
@relation_factory = relation_factory
|
||||
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
bulk_inserts_enabled = @importable.class == ::Project &&
|
||||
Feature.enabled?(:import_bulk_inserts, @importable.group)
|
||||
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_inserts_enabled) do
|
||||
update_relation_hashes!
|
||||
fix_ci_pipelines_not_sorted_on_legacy_project_json!
|
||||
create_relations!
|
||||
end
|
||||
end
|
||||
|
@ -57,18 +57,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def process_relation!(relation_key, relation_definition)
|
||||
data_hashes = @tree_hash.delete(relation_key)
|
||||
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
|
||||
@relation_reader.consume_relation(relation_key) do |data_hash, relation_index|
|
||||
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
|
||||
relation_index += 1
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -103,10 +93,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def update_params!
|
||||
params = @tree_hash.reject do |key, _|
|
||||
relations.include?(key)
|
||||
end
|
||||
|
||||
params = @relation_reader.root_attributes(relations.keys)
|
||||
params = params.merge(present_override_params)
|
||||
|
||||
# Cleaning all imported and overridden params
|
||||
|
@ -223,8 +210,13 @@ module Gitlab
|
|||
}
|
||||
end
|
||||
|
||||
def update_relation_hashes!
|
||||
@tree_hash['ci_pipelines']&.sort_by! { |hash| hash['id'] }
|
||||
# Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
|
||||
# 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
|
||||
|
|
|
@ -18,7 +18,7 @@ module Gitlab
|
|||
def save(tree, dir_path, filename)
|
||||
mkdir_p(dir_path)
|
||||
|
||||
tree_json = JSON.generate(tree)
|
||||
tree_json = ::JSON.generate(tree)
|
||||
|
||||
File.write(File.join(dir_path, filename), tree_json)
|
||||
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 { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import Mousetrap from 'mousetrap';
|
||||
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 { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
|
||||
import createDiffsStore from '../create_diffs_store';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import diffsMockData from '../mock_data/merge_request_diffs';
|
||||
|
||||
const mergeRequestDiff = { version_index: 1 };
|
||||
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
|
||||
|
||||
describe('diffs/components/app', () => {
|
||||
const oldMrTabs = window.mrTabs;
|
||||
let store;
|
||||
let wrapper;
|
||||
let mock;
|
||||
|
||||
function createComponent(props = {}, extendStore = () => {}) {
|
||||
const localVue = createLocalVue();
|
||||
|
@ -34,7 +38,7 @@ describe('diffs/components/app', () => {
|
|||
wrapper = shallowMount(localVue.extend(App), {
|
||||
localVue,
|
||||
propsData: {
|
||||
endpoint: `${TEST_HOST}/diff/endpoint`,
|
||||
endpoint: TEST_ENDPOINT,
|
||||
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
|
||||
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
|
||||
projectPath: 'namespace/project',
|
||||
|
@ -61,8 +65,12 @@ describe('diffs/components/app', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// setup globals (needed for component to mount :/)
|
||||
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
|
||||
window.mrTabs.expandViewContainer = jasmine.createSpy();
|
||||
window.mrTabs = {
|
||||
resetViewContainer: jest.fn(),
|
||||
};
|
||||
window.mrTabs.expandViewContainer = jest.fn();
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(TEST_ENDPOINT).reply(200, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -71,6 +79,8 @@ describe('diffs/components/app', () => {
|
|||
|
||||
// reset component
|
||||
wrapper.destroy();
|
||||
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('fetch diff methods', () => {
|
||||
|
@ -80,15 +90,15 @@ describe('diffs/components/app', () => {
|
|||
store.state.notes.discussions = 'test';
|
||||
return Promise.resolve({ real_size: 100 });
|
||||
};
|
||||
spyOn(window, 'requestIdleCallback').and.callFake(fn => fn());
|
||||
jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
|
||||
createComponent();
|
||||
spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(fetchResolver);
|
||||
spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(fetchResolver);
|
||||
spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(fetchResolver);
|
||||
spyOn(wrapper.vm, 'setDiscussions');
|
||||
spyOn(wrapper.vm, 'startRenderDiffsQueue');
|
||||
spyOn(wrapper.vm, 'unwatchDiscussions');
|
||||
spyOn(wrapper.vm, 'unwatchRetrievingBatches');
|
||||
jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver);
|
||||
jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver);
|
||||
jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
|
||||
jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm, 'startRenderDiffsQueue').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
|
||||
jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
|
||||
store.state.diffs.retrievingBatches = true;
|
||||
store.state.diffs.diffFiles = [];
|
||||
wrapper.vm.$nextTick(done);
|
||||
|
@ -236,7 +246,7 @@ describe('diffs/components/app', () => {
|
|||
wrapper.vm.fetchData(false);
|
||||
|
||||
expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled();
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
|
||||
|
@ -255,7 +265,7 @@ describe('diffs/components/app', () => {
|
|||
wrapper.vm.fetchData(false);
|
||||
|
||||
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
||||
|
@ -272,7 +282,7 @@ describe('diffs/components/app', () => {
|
|||
wrapper.vm.fetchData(false);
|
||||
|
||||
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
|
||||
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
|
||||
|
@ -350,23 +360,21 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
|
||||
// Component uses $nextTick so we wait until that has finished
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('marks current diff file based on currently highlighted row', done => {
|
||||
it('marks current diff file based on currently highlighted row', () => {
|
||||
createComponent({
|
||||
shouldShow: true,
|
||||
});
|
||||
|
||||
// Component uses $nextTick so we wait until that has finished
|
||||
setTimeout(() => {
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
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
|
||||
setTimeout(() => {
|
||||
setImmediate(() => {
|
||||
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
|
||||
|
||||
done();
|
||||
|
@ -449,7 +457,7 @@ describe('diffs/components/app', () => {
|
|||
|
||||
describe('visible app', () => {
|
||||
beforeEach(() => {
|
||||
spy = jasmine.createSpy('spy');
|
||||
spy = jest.fn();
|
||||
|
||||
createComponent({
|
||||
shouldShow: true,
|
||||
|
@ -459,21 +467,18 @@ describe('diffs/components/app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => {
|
||||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
Object.keys(mappings).forEach(function(key) {
|
||||
Mousetrap.trigger(key);
|
||||
it.each(Object.keys(mappings))(
|
||||
'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed',
|
||||
key => {
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
||||
expect(spy.calls.mostRecent().args).toEqual([mappings[key]]);
|
||||
});
|
||||
Mousetrap.trigger(key);
|
||||
|
||||
expect(spy.calls.count()).toEqual(Object.keys(mappings).length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith(mappings[key]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('does not call `jumpToFile()` when unknown key is pressed', done => {
|
||||
wrapper.vm
|
||||
|
@ -490,7 +495,7 @@ describe('diffs/components/app', () => {
|
|||
|
||||
describe('hideen app', () => {
|
||||
beforeEach(() => {
|
||||
spy = jasmine.createSpy('spy');
|
||||
spy = jest.fn();
|
||||
|
||||
createComponent({
|
||||
shouldShow: false,
|
||||
|
@ -504,7 +509,7 @@ describe('diffs/components/app', () => {
|
|||
wrapper.vm
|
||||
.$nextTick()
|
||||
.then(() => {
|
||||
Object.keys(mappings).forEach(function(key) {
|
||||
Object.keys(mappings).forEach(key => {
|
||||
Mousetrap.trigger(key);
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
|
@ -520,7 +525,7 @@ describe('diffs/components/app', () => {
|
|||
let spy;
|
||||
|
||||
beforeEach(() => {
|
||||
spy = jasmine.createSpy();
|
||||
spy = jest.fn();
|
||||
|
||||
createComponent({}, () => {
|
||||
store.state.diffs.diffFiles = [
|
||||
|
@ -545,15 +550,15 @@ describe('diffs/components/app', () => {
|
|||
.then(() => {
|
||||
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';
|
||||
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';
|
||||
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)
|
||||
.catch(done.fail);
|
||||
|
@ -602,7 +607,7 @@ describe('diffs/components/app', () => {
|
|||
|
||||
expect(wrapper.contains(CompareVersions)).toBe(true);
|
||||
expect(wrapper.find(CompareVersions).props()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
targetBranch: {
|
||||
branchName: 'target-branch',
|
||||
versionIndex: -1,
|
||||
|
@ -625,7 +630,7 @@ describe('diffs/components/app', () => {
|
|||
|
||||
expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
|
||||
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
expect.objectContaining({
|
||||
total: '5',
|
||||
plainDiffPath: 'plain diff path',
|
||||
emailPatchPath: 'email patch path',
|
||||
|
@ -663,7 +668,7 @@ describe('diffs/components/app', () => {
|
|||
let toggleShowTreeList;
|
||||
|
||||
beforeEach(() => {
|
||||
toggleShowTreeList = jasmine.createSpy('toggleShowTreeList');
|
||||
toggleShowTreeList = jest.fn();
|
||||
});
|
||||
|
||||
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
|
||||
cansetci="true"
|
||||
class="mb-auto mt-auto"
|
||||
emptystatesvgpath="http://test.host"
|
||||
helppagepath="http://test.host"
|
||||
/>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
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('getSvgIconPathContent', () => {
|
||||
let spriteIcons;
|
||||
let axiosMock;
|
||||
const mockName = 'mockIconName';
|
||||
const mockPath = 'mockPath';
|
||||
const mockIcons = `<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`;
|
||||
|
||||
beforeAll(() => {
|
||||
spriteIcons = gon.sprite_icons;
|
||||
|
@ -15,45 +19,63 @@ describe('Icon utils', () => {
|
|||
gon.sprite_icons = spriteIcons;
|
||||
});
|
||||
|
||||
let axiosMock;
|
||||
let mockEndpoint;
|
||||
const mockName = 'mockIconName';
|
||||
const mockPath = 'mockPath';
|
||||
const getIcon = () => iconUtils.getSvgIconPathContent(mockName);
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
mockEndpoint = axiosMock.onGet(gon.sprite_icons);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
clearSvgIconPathContentCache();
|
||||
});
|
||||
|
||||
it('extracts svg icon path content from sprite icons', () => {
|
||||
mockEndpoint.replyOnce(
|
||||
200,
|
||||
`<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`,
|
||||
);
|
||||
describe('when the icons can be loaded', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons);
|
||||
});
|
||||
|
||||
return getIcon().then(path => {
|
||||
expect(path).toBe(mockPath);
|
||||
it('extracts svg icon path content from sprite icons', () => {
|
||||
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', () => {
|
||||
mockEndpoint.replyOnce(200, ``);
|
||||
|
||||
return getIcon().then(path => {
|
||||
expect(path).toBe(null);
|
||||
describe('when the icons cannot be loaded on the first 2 tries', () => {
|
||||
beforeEach(() => {
|
||||
axiosMock
|
||||
.onGet(gon.sprite_icons)
|
||||
.replyOnce(500)
|
||||
.onGet(gon.sprite_icons)
|
||||
.replyOnce(500)
|
||||
.onGet(gon.sprite_icons)
|
||||
.reply(200, mockIcons);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if an http error occurs', () => {
|
||||
mockEndpoint.replyOnce(500);
|
||||
it('returns null', () => {
|
||||
return getSvgIconPathContent(mockName).then(path => {
|
||||
expect(path).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
return getIcon().then(path => {
|
||||
expect(path).toBe(null);
|
||||
it('extracts svg icon path content, after 2 attempts', () => {
|
||||
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';
|
||||
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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
export { default } from '../../frontend/diffs/create_diffs_store';
|
||||
|
|
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
context 'no group visibility' do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# 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.
|
||||
# This spec tests that restore project works
|
||||
|
@ -25,7 +25,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
described_class.new(
|
||||
user: user,
|
||||
shared: shared,
|
||||
tree_hash: tree_hash,
|
||||
relation_reader: relation_reader,
|
||||
importable: importable,
|
||||
object_builder: object_builder,
|
||||
members_mapper: members_mapper,
|
||||
|
@ -36,14 +36,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
|
||||
subject { relation_tree_restorer.restore }
|
||||
|
||||
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) }
|
||||
let(:tree_hash) { importable_hash }
|
||||
|
||||
shared_examples 'import project successfully' do
|
||||
it 'restores project tree' do
|
||||
expect(subject).to eq(true)
|
||||
end
|
||||
|
@ -66,4 +59,18 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
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
|
||||
|
|
|
@ -625,4 +625,14 @@ describe SystemNoteService do
|
|||
described_class.discussion_lock(issuable, double)
|
||||
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
|
||||
|
|
|
@ -654,4 +654,16 @@ describe ::SystemNotes::IssuablesService do
|
|||
.to eq('resolved the corresponding error and closed the issue.')
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue