Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-13 18:08:58 +00:00
parent 16515bdfcb
commit b4b6bff01d
56 changed files with 1072 additions and 656 deletions

View File

@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
gem 'rack-proxy', '~> 0.6.0'
gem 'sassc-rails', '~> 2.1.0'
gem 'gitlab-terser', '1.0.1.1'
gem 'terser', '1.0.2'
gem 'addressable', '~> 2.7'
gem 'font-awesome-rails', '~> 4.7'

View File

@ -452,8 +452,6 @@ GEM
rubocop-performance (~> 1.5.2)
rubocop-rails (~> 2.5)
rubocop-rspec (~> 1.36)
gitlab-terser (1.0.1.1)
execjs (>= 0.3.0, < 3)
gitlab_chronic_duration (0.10.6.2)
numerizer (~> 0.2)
gitlab_omniauth-ldap (2.1.1)
@ -1132,6 +1130,8 @@ GEM
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terser (1.0.2)
execjs (>= 0.3.0, < 3)
test-prof (0.12.0)
text (1.3.1)
thin (1.7.2)
@ -1333,7 +1333,6 @@ DEPENDENCIES
gitlab-puma_worker_killer (~> 0.1.1.gitlab.1)
gitlab-sidekiq-fetcher (= 0.5.2)
gitlab-styles (~> 4.3.0)
gitlab-terser (= 1.0.1.1)
gitlab_chronic_duration (~> 0.10.6.2)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
@ -1483,6 +1482,7 @@ DEPENDENCIES
stackprof (~> 0.2.15)
state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
terser (= 1.0.2)
test-prof (~> 0.12.0)
thin (~> 1.7.0)
timecop (~> 0.9.1)

View File

@ -3,9 +3,7 @@ import isEmojiUnicodeSupported from '../emoji/support';
import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
class GlEmoji extends HTMLElement {
constructor() {
super();
connectedCallback() {
this.initialize();
}
initialize() {

View File

@ -9,7 +9,6 @@ import {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
} from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -19,6 +18,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
import { DIFF_FILE_HEADER } from '../i18n';
export default {
components: {
@ -30,13 +30,15 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
i18n: {
...DIFF_FILE_HEADER,
},
props: {
discussionPath: {
type: String,
@ -290,7 +292,7 @@ export default {
icon="external-link"
/>
<gl-dropdown
v-gl-tooltip.hover.focus="__('More actions')"
v-gl-tooltip.hover.focus="$options.i18n.optionsDropdownTitle"
right
toggle-class="btn-icon js-diff-more-actions"
class="gl-pt-0!"
@ -299,11 +301,8 @@ export default {
>
<template #button-content>
<gl-icon name="ellipsis_v" class="mr-0" />
<span class="sr-only">{{ __('More actions') }}</span>
<span class="sr-only">{{ $options.i18n.optionsDropdownTitle }}</span>
</template>
<gl-dropdown-section-header>
{{ __('More actions') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-if="diffFile.replaced_view_path"
ref="replacedFileButton"

View File

@ -0,0 +1,5 @@
import { __ } from '~/locale';
export const DIFF_FILE_HEADER = {
optionsDropdownTitle: __('Options'),
};

View File

@ -14,14 +14,11 @@ import { createUniqueJobId } from '../../utils';
export const generateLinksData = ({ links }, jobs, containerID) => {
const containerEl = document.getElementById(containerID);
return links.map(link => {
const path = d3.path();
// We can only have one unique job name per stage, so our selector
// is: ${stageName}-${jobName}
const sourceId = createUniqueJobId(jobs[link.source].stage, link.source);
const targetId = createUniqueJobId(jobs[link.target].stage, link.target);
const sourceId = jobs[link.source].id;
const targetId = jobs[link.target].id;
const sourceNodeEl = document.getElementById(sourceId);
const targetNodeEl = document.getElementById(targetId);
@ -80,6 +77,12 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
targetNodeY,
);
return { ...link, path: path.toString() };
return {
...link,
source: sourceId,
target: targetId,
ref: createUniqueJobId(sourceId, targetId),
path: path.toString(),
};
});
};

View File

@ -14,6 +14,42 @@ export default {
type: String,
required: true,
},
isHighlighted: {
type: Boolean,
required: false,
default: false,
},
isFadedOut: {
type: Boolean,
required: false,
default: false,
},
handleMouseOver: {
type: Function,
required: false,
default: () => {},
},
handleMouseLeave: {
type: Function,
required: false,
default: () => {},
},
},
computed: {
jobPillClasses() {
return [
{ 'gl-opacity-3': this.isFadedOut },
this.isHighlighted ? 'gl-shadow-blue-200-x0-y0-b4-s2' : 'gl-inset-border-2-green-400',
];
},
},
methods: {
onMouseEnter() {
this.$emit('on-mouse-enter', this.jobId);
},
onMouseLeave() {
this.$emit('on-mouse-leave');
},
},
};
</script>
@ -21,7 +57,10 @@ export default {
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
<div
:id="jobId"
class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 pipeline-job-pill "
class="pipeline-job-pill gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class="jobPillClasses"
@mouseover="onMouseEnter"
@mouseleave="onMouseLeave"
>
{{ jobName }}
</div>

View File

@ -7,7 +7,7 @@ import StagePill from './stage_pill.vue';
import { generateLinksData } from './drawing_utils';
import { parseData } from '../parsing_utils';
import { DRAW_FAILURE, DEFAULT } from '../../constants';
import { createUniqueJobId } from '../../utils';
import { generateJobNeedsDict } from '../../utils';
export default {
components: {
@ -31,7 +31,9 @@ export default {
data() {
return {
failureType: null,
highlightedJob: null,
links: [],
needsObject: null,
height: 0,
width: 0,
};
@ -43,6 +45,9 @@ export default {
hasError() {
return this.failureType;
},
hasHighlightedJob() {
return Boolean(this.highlightedJob);
},
failure() {
const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
@ -51,8 +56,27 @@ export default {
viewBox() {
return [0, 0, this.width, this.height];
},
lineStyle() {
return `stroke-width:${this.$options.STROKE_WIDTH}px;`;
highlightedJobs() {
// If you are hovering on a job, then the jobs we want to highlight are:
// The job you are currently hovering + all of its needs.
return this.hasHighlightedJob
? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
: [];
},
highlightedLinks() {
// If you are hovering on a job, then the links we want to highlight are:
// All the links whose `source` and `target` are highlighted jobs.
if (this.hasHighlightedJob) {
const filteredLinks = this.links.filter(link => {
return (
this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
);
});
return filteredLinks.map(link => link.ref);
}
return [];
},
},
mounted() {
@ -62,9 +86,6 @@ export default {
}
},
methods: {
createJobId(stageName, jobName) {
return createUniqueJobId(stageName, jobName);
},
drawJobLinks() {
const { stages, jobs } = this.pipelineData;
const unwrappedGroups = this.unwrapPipelineData(stages);
@ -76,6 +97,18 @@ export default {
this.reportFailure(DRAW_FAILURE);
}
},
highlightNeeds(uniqueJobId) {
// The first time we hover, we create the object where
// we store all the data to properly highlight the needs.
if (!this.needsObject) {
this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
}
this.highlightedJob = uniqueJobId;
},
removeHighlightNeeds() {
this.highlightedJob = null;
},
unwrapPipelineData(stages) {
return stages
.map(({ name, groups }) => {
@ -95,6 +128,18 @@ export default {
resetFailure() {
this.failureType = null;
},
isJobHighlighted(jobName) {
return this.highlightedJobs.includes(jobName);
},
isLinkHighlighted(linkRef) {
return this.highlightedLinks.includes(linkRef);
},
getLinkClasses(link) {
return [
this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
{ 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
];
},
},
};
</script>
@ -113,13 +158,17 @@ export default {
class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
>
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
<path
v-for="link in links"
:key="link.path"
:d="link.path"
class="gl-stroke-gray-200 gl-fill-transparent"
:style="lineStyle"
/>
<template>
<path
v-for="link in links"
:key="link.path"
:ref="link.ref"
:d="link.path"
class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
:class="getLinkClasses(link)"
:stroke-width="$options.STROKE_WIDTH"
/>
</template>
</svg>
<div
v-for="(stage, index) in pipelineData.stages"
@ -141,8 +190,12 @@ export default {
<job-pill
v-for="group in stage.groups"
:key="group.name"
:job-id="createJobId(stage.name, group.name)"
:job-id="group.id"
:job-name="group.name"
:is-highlighted="hasHighlightedJob && isJobHighlighted(group.id)"
:is-faded-out="hasHighlightedJob && !isJobHighlighted(group.id)"
@on-mouse-enter="highlightNeeds"
@on-mouse-leave="removeHighlightNeeds"
/>
</div>
</div>

View File

@ -5,6 +5,8 @@ export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
/**
* This function takes a json payload that comes from a yml
* file converted to json through `jsyaml` library. Because we
@ -21,7 +23,10 @@ export const preparePipelineGraphData = jsonData => {
// Creates an object with only the valid jobs
const jobs = jsonKeys.reduce((acc, val) => {
if (jobNames.includes(val)) {
return { ...acc, [val]: { ...jsonData[val] } };
return {
...acc,
[val]: { ...jsonData[val], id: createUniqueJobId(jsonData[val].stage, val) },
};
}
return { ...acc };
}, {});
@ -47,7 +52,11 @@ export const preparePipelineGraphData = jsonData => {
return {
name: stage,
groups: stageJobs.map(job => {
return { name: job, jobs: [{ ...jsonData[job] }] };
return {
name: job,
jobs: [{ ...jsonData[job] }],
id: createUniqueJobId(stage, job),
};
}),
};
});
@ -55,4 +64,33 @@ export const preparePipelineGraphData = jsonData => {
return { stages: pipelineData, jobs };
};
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
export const generateJobNeedsDict = ({ jobs }) => {
const arrOfJobNames = Object.keys(jobs);
return arrOfJobNames.reduce((acc, value) => {
const recursiveNeeds = jobName => {
if (!jobs[jobName]?.needs) {
return [];
}
return jobs[jobName].needs
.map(job => {
const { id } = jobs[job];
// If we already have the needs of a job in the accumulator,
// then we use the memoized data instead of the recursive call
// to save some performance.
const newNeeds = acc[id] ?? recursiveNeeds(job);
return [id, ...newNeeds];
})
.flat(Infinity);
};
// To ensure we don't have duplicates job relationship when 2 jobs
// needed by another both depends on the same jobs, we remove any
// duplicates from the array.
const uniqueValues = Array.from(new Set(recursiveNeeds(value)));
return { ...acc, [jobs[value].id]: uniqueValues };
}, {});
};

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
@ -11,8 +11,8 @@ import { sprintf } from '~/locale';
export default {
components: {
BlobHeaderEdit,
BlobContentEdit,
GlLoadingIcon,
EditorLite,
},
inheritAttrs: false,
props: {
@ -85,7 +85,7 @@ export default {
size="lg"
class="loading-animation prepend-top-20 gl-mb-6"
/>
<blob-content-edit
<editor-lite
v-else
:value="blob.content"
:file-global-id="blob.id"

View File

@ -9,7 +9,7 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
respectDoNotTrack: true,
forceSecureTracker: true,
eventMethod: 'post',
contexts: { webPage: true },
contexts: { webPage: true, performanceTiming: true },
formTracking: false,
linkClickTracking: false,
};

View File

@ -24,3 +24,210 @@
color: $gl-text-color;
}
}
@mixin mini-pipeline-graph-color(
$color-background-default,
$color-background-hover-focus,
$color-background-active,
$color-foreground-default,
$color-foreground-hover-focus,
$color-foreground-active
) {
background-color: $color-background-default;
border-color: $color-foreground-default;
svg {
fill: $color-foreground-default;
}
&:hover,
&:focus {
background-color: $color-background-hover-focus;
border-color: $color-foreground-hover-focus;
svg {
fill: $color-foreground-hover-focus;
}
}
&:active {
background-color: $color-background-active;
border-color: $color-foreground-active;
svg {
fill: $color-foreground-active;
}
}
&:focus {
box-shadow: 0 0 4px 1px $blue-300;
}
}
@mixin mini-pipeline-item() {
border-radius: 100px;
background-color: $white;
border-width: 1px;
border-style: solid;
width: $ci-action-icon-size;
height: $ci-action-icon-size;
margin: 0;
padding: 0;
position: relative;
vertical-align: middle;
&:hover,
&:active,
&:focus {
outline: none;
border-width: 2px;
}
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700);
}
&.ci-status-icon-failed {
@include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700);
}
&.ci-status-icon-pending,
&.ci-status-icon-waiting-for-resource,
&.ci-status-icon-success-with-warnings {
@include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
}
&.ci-status-icon-preparing,
&.ci-status-icon-running {
@include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
}
&.ci-status-icon-canceled,
&.ci-status-icon-scheduled,
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
@include mini-pipeline-graph-color($white, $gray-500, $gray-700, $gray-900, $gray-950, $black);
}
&.ci-status-icon-created,
&.ci-status-icon-skipped {
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
}
}
/**
Action icons inside dropdowns:
- mini graph in pipelines table
- dropdown in big graph
- mini graph in MR widget pipeline
- mini graph in Commit widget pipeline
*/
@mixin pipeline-graph-dropdown-menu() {
width: 240px;
max-width: 240px;
// override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0;
text-align: center;
}
.ci-action-icon-container {
position: absolute;
right: 8px;
top: 8px;
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
width: $ci-action-dropdown-button-size;
border-radius: 50%;
display: block;
&:hover {
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
background-color: $gray-darker;
svg {
fill: $gl-text-color;
}
}
.spinner,
svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
top: 1px;
vertical-align: initial;
}
}
}
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
top: 5px;
}
.scrollable-menu {
padding: 0;
max-height: 245px;
overflow: auto;
}
li {
position: relative;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
&:hover > .mini-pipeline-graph-dropdown-item,
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
@extend .mini-pipeline-graph-dropdown-item:hover;
}
// link to the build
.mini-pipeline-graph-dropdown-item {
align-items: center;
clear: both;
display: flex;
font-weight: normal;
line-height: $line-height-base;
white-space: nowrap;
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
padding: $gl-padding-8 $gl-btn-horz-padding;
}
.ci-job-name-component {
align-items: center;
display: flex;
flex: 1;
}
.ci-status-icon {
@include gl-mr-3;
position: relative;
> svg {
width: $pipeline-dropdown-status-icon-size;
height: $pipeline-dropdown-status-icon-size;
margin: 3px 0;
position: relative;
overflow: visible;
display: block;
}
}
&:hover,
&:focus {
outline: none;
text-decoration: none;
background-color: $gray-darker;
}
}
}
}

View File

@ -386,7 +386,6 @@
top: 8px;
}
.split-report-section {
border-bottom: 1px solid var(--gray-50, $gray-50);
@ -427,6 +426,43 @@
}
}
.big-pipeline-graph-dropdown-menu {
@include pipeline-graph-dropdown-menu();
width: 195px;
min-width: 195px;
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
/**
* Top arrow in the dropdown in the big pipeline graph
*/
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -6px;
margin-top: 3px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -5px;
border-width: 10px 7px 10px 0;
border-right-color: $white;
}
}
.codequality-report {
.media {
padding: $gl-padding;

View File

@ -1,14 +1,14 @@
@import 'mixins_and_variables_and_functions';
@import './pipeline_mixins';
/**
* Pipelines Bundle
*
* Styles of pipeline lists
*
* Should affect pipelines table components rendered by:
* app/assets/javascripts/commit/pipelines/pipelines_bundle.js
* Pipelines Bundle: Pipeline lists and Mini Pipelines
*/
// Pipelines list
// Should affect pipelines table components rendered by:
// - app/assets/javascripts/commit/pipelines/pipelines_bundle.js
.pipelines {
.badge {
margin-bottom: 3px;
@ -64,3 +64,132 @@
white-space: normal;
}
}
// Mini Pipelines
.stage-cell {
.mini-pipeline-graph-dropdown-toggle {
svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
&:hover,
&:active,
&:focus {
svg {
top: -2px;
left: -2px;
}
}
}
.stage-container {
display: inline-block;
position: relative;
vertical-align: middle;
height: $ci-action-icon-size;
margin: 3px 0;
+ .stage-container {
margin-left: 6px;
}
// Hack to show a button tooltip inline
button.has-tooltip + .tooltip {
min-width: 105px;
}
// Bootstrap way of showing the content inline for anchors.
a.has-tooltip {
white-space: nowrap;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 11px;
border-bottom: 2px solid $border-color;
}
}
//delete when all pipelines are updated to new size
&.mr-widget-pipeline-stages {
+ .stage-container {
margin-left: 4px;
}
&:not(:last-child) {
&::after {
width: 4px;
right: -4px;
top: 11px;
}
}
}
}
}
// Dropdown button in mini pipeline graph
button.mini-pipeline-graph-dropdown-toggle {
@include mini-pipeline-item();
}
// Action icons inside dropdowns:
// mini graph in pipelines table
// mini graph in MR widget pipeline
// mini graph in Commit widget pipeline
.mini-pipeline-graph-dropdown-menu {
@include pipeline-graph-dropdown-menu();
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 50%;
transform: translate(-50%, 0);
border-width: 0 5px 6px;
@include media-breakpoint-down(sm) {
left: 100%;
margin-left: -12px;
}
}
&::before {
border-width: 0 5px 5px;
border-bottom-color: $border-color;
}
&::after {
margin-top: 1px;
border-bottom-color: $white;
}
/**
* Center dropdown menu in mini graph
*/
.dropdown &.dropdown-menu {
transform: translate(-80%, 0);
@media (min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
}
}
}

View File

@ -78,77 +78,6 @@
}
}
.stage-cell {
.mini-pipeline-graph-dropdown-toggle {
svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
&:hover,
&:active,
&:focus {
svg {
top: -2px;
left: -2px;
}
}
}
.stage-container {
display: inline-block;
position: relative;
vertical-align: middle;
height: $ci-action-icon-size;
margin: 3px 0;
+ .stage-container {
margin-left: 6px;
}
// Hack to show a button tooltip inline
button.has-tooltip + .tooltip {
min-width: 105px;
}
// Bootstrap way of showing the content inline for anchors.
a.has-tooltip {
white-space: nowrap;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 11px;
border-bottom: 2px solid $border-color;
}
}
//delete when all pipelines are updated to new size
&.mr-widget-pipeline-stages {
+ .stage-container {
margin-left: 4px;
}
&:not(:last-child) {
&::after {
width: 4px;
right: -4px;
top: 11px;
}
}
}
}
}
[data-page='admin:jobs:index'] {
.admin-builds-table {
td:last-child {
@ -162,305 +91,6 @@
font-weight: 200;
}
@mixin mini-pipeline-graph-color(
$color-background-default,
$color-background-hover-focus,
$color-background-active,
$color-foreground-default,
$color-foreground-hover-focus,
$color-foreground-active
) {
background-color: $color-background-default;
border-color: $color-foreground-default;
svg {
fill: $color-foreground-default;
}
&:hover,
&:focus {
background-color: $color-background-hover-focus;
border-color: $color-foreground-hover-focus;
svg {
fill: $color-foreground-hover-focus;
}
}
&:active {
background-color: $color-background-active;
border-color: $color-foreground-active;
svg {
fill: $color-foreground-active;
}
}
&:focus {
box-shadow: 0 0 4px 1px $blue-300;
}
}
@mixin mini-pipeline-item() {
border-radius: 100px;
background-color: $white;
border-width: 1px;
border-style: solid;
width: $ci-action-icon-size;
height: $ci-action-icon-size;
margin: 0;
padding: 0;
position: relative;
vertical-align: middle;
&:hover,
&:active,
&:focus {
outline: none;
border-width: 2px;
}
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700);
}
&.ci-status-icon-failed {
@include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700);
}
&.ci-status-icon-pending,
&.ci-status-icon-waiting-for-resource,
&.ci-status-icon-success-with-warnings {
@include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
}
&.ci-status-icon-preparing,
&.ci-status-icon-running {
@include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
}
&.ci-status-icon-canceled,
&.ci-status-icon-scheduled,
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
@include mini-pipeline-graph-color($white, $gray-500, $gray-700, $gray-900, $gray-950, $black);
}
&.ci-status-icon-created,
&.ci-status-icon-skipped {
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
}
}
// Dropdown button in mini pipeline graph
button.mini-pipeline-graph-dropdown-toggle {
@include mini-pipeline-item();
}
/**
Action icons inside dropdowns:
- mini graph in pipelines table
- dropdown in big graph
- mini graph in MR widget pipeline
- mini graph in Commit widget pipeline
*/
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 240px;
max-width: 240px;
// override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0;
text-align: center;
}
.ci-action-icon-container {
position: absolute;
right: 8px;
top: 8px;
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
width: $ci-action-dropdown-button-size;
border-radius: 50%;
display: block;
&:hover {
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
background-color: $gray-darker;
svg {
fill: $gl-text-color;
}
}
.spinner,
svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
top: 1px;
vertical-align: initial;
}
}
}
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
top: 5px;
}
.scrollable-menu {
padding: 0;
max-height: 245px;
overflow: auto;
}
li {
position: relative;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
&:hover > .mini-pipeline-graph-dropdown-item,
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
@extend .mini-pipeline-graph-dropdown-item:hover;
}
// link to the build
.mini-pipeline-graph-dropdown-item {
align-items: center;
clear: both;
display: flex;
font-weight: normal;
line-height: $line-height-base;
white-space: nowrap;
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
padding: $gl-padding-8 $gl-btn-horz-padding;
}
.ci-job-name-component {
align-items: center;
display: flex;
flex: 1;
}
.ci-status-icon {
@include gl-mr-3;
position: relative;
> svg {
width: $pipeline-dropdown-status-icon-size;
height: $pipeline-dropdown-status-icon-size;
margin: 3px 0;
position: relative;
overflow: visible;
display: block;
}
}
&:hover,
&:focus {
outline: none;
text-decoration: none;
background-color: $gray-darker;
}
}
}
}
// Dropdown in the big pipeline graph
.big-pipeline-graph-dropdown-menu {
width: 195px;
min-width: 195px;
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
/**
* Top arrow in the dropdown in the big pipeline graph
*/
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -6px;
margin-top: 3px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -5px;
border-width: 10px 7px 10px 0;
border-right-color: $white;
}
}
/**
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: -6px;
left: 50%;
transform: translate(-50%, 0);
border-width: 0 5px 6px;
@include media-breakpoint-down(sm) {
left: 100%;
margin-left: -12px;
}
}
&::before {
border-width: 0 5px 5px;
border-bottom-color: $border-color;
}
&::after {
margin-top: 1px;
border-bottom-color: $white;
}
/**
* Center dropdown menu in mini graph
*/
.dropdown &.dropdown-menu {
transform: translate(-80%, 0);
@media (min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
}
}
}
/**
* Terminal
*/

View File

@ -27,6 +27,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
).execute
if service_response.success?
Gitlab::UsageDataCounters::StaticSiteEditorCounter.increment_views_count
@data = serialize_necessary_payload_values_to_json(service_response.payload)
else
# TODO: For now, if the service returns any error, the user is redirected

View File

@ -44,6 +44,7 @@ module ResolvesMergeRequests
author: [:author],
merged_at: [:metrics],
commit_count: [:metrics],
diff_stats_summary: [:metrics],
approved_by: [:approved_by_users],
milestone: [:milestone],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]

View File

@ -170,6 +170,12 @@ module Types
end
def diff_stats_summary
metrics = object.metrics
if metrics && metrics.added_lines && metrics.removed_lines
return { additions: metrics.added_lines, deletions: metrics.removed_lines, file_count: object.merge_request_diff&.files_count || 0 }
end
nil_stats = { additions: 0, deletions: 0, file_count: 0 }
return nil_stats unless object.diff_stats.present?

View File

@ -40,7 +40,12 @@ module Jira
build_service_response(response)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
error_message = "Jira request error: #{error.message}"
log_error("Error sending message", client_url: client.options[:site], error: error_message)
log_error("Error sending message", client_url: client.options[:site],
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
})
ServiceResponse.error(message: error_message)
end

View File

@ -3,6 +3,8 @@
module Packages
module Composer
class ComposerJsonService
InvalidJson = Class.new(StandardError)
def initialize(project, target)
@project, @target = project, target
end
@ -20,11 +22,11 @@ module Packages
Gitlab::Json.parse(composer_file.data)
rescue JSON::ParserError
raise 'Could not parse composer.json file. Invalid JSON.'
raise InvalidJson, 'Could not parse composer.json file. Invalid JSON.'
end
def composer_file_not_found!
raise 'The file composer.json was not found.'
raise InvalidJson, 'The file composer.json was not found.'
end
end
end

View File

@ -7,6 +7,7 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", _('Commits')
- page_description @commit.description
- add_page_specific_style 'page_bundles/pipelines'
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"

View File

@ -0,0 +1,5 @@
---
title: Replaced blob-content-edit with editor-lite compoennt for Snippet edit form
merge_request: 44994
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add usage ping to count Static Site Editor views
merge_request: 44573
author:
type: added

View File

@ -0,0 +1,6 @@
---
title: Optionally use merge request metrics association for merge request diff stats
in GraphQL
merge_request: 44613
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Return 422 error rather than 500 when composer.json is missing or malformed
merge_request: 44587
author: David Barr @davebarr
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix emoji rendering in certain edge cases
merge_request: 44542
author:
type: fixed

View File

@ -1,7 +0,0 @@
---
name: ci_bridge_pipeline_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41263
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/250683
group: group::memory
type: development
default_enabled: true

View File

@ -1,3 +1 @@
require 'terser'
Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor

View File

@ -8,8 +8,7 @@ docs_paths_to_review = helper.changes_by_category[:docs]
return if docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
'from the Technical Writing team.'
message 'This merge request adds or changes documentation files. A review from the Technical Writing team before you merge is **recommended**. Reviews can happen after you merge.'
return unless gitlab_danger.ci?

View File

@ -53,8 +53,14 @@ supporting custom domains a secondary IP is not needed.
Before proceeding with the Pages configuration, you will need to:
1. Have an exclusive root domain for serving GitLab Pages. Note that you cannot
use a subdomain of your GitLab's instance domain.
1. Have a domain for Pages that is not a subdomain of your GitLab's instance domain.
| GitLab domain | Pages domain | Does it work? |
| :---: | :---: | :---: |
| `example.com` | `example.io` | **{check-circle}** Yes |
| `example.com` | `pages.example.com` | **{dotted-circle}** No |
| `gitlab.example.com` | `pages.example.com` | **{check-circle}** Yes |
1. Configure a **wildcard DNS record**.
1. (Optional) Have a **wildcard certificate** for that domain if you decide to
serve Pages under HTTPS.

View File

@ -3584,10 +3584,11 @@ job split into three separate jobs.
Use `matrix:` to configure different variables for jobs that are running in parallel.
There can be from 2 to 50 jobs.
In GitLab 13.5 and later, you can have one-dimensional matrices with a single job.
[In GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/26362) and later,
you can have one-dimensional matrices with a single job.
The ability to have one-dimensional matrices is [deployed behind a feature flag](../../user/feature_flags.md),
disabled by default. It's disabled on GitLab.com. To use it in a GitLab self-managed
instance, ask a GitLab administrator to [enable the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE-ONLY)**
instance, ask a GitLab administrator to [enable the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE ONLY)**
Every job gets the same `CI_NODE_TOTAL` [environment variable](../variables/README.md#predefined-environment-variables) value, and a unique `CI_NODE_INDEX` value.

View File

@ -26,6 +26,10 @@ module API
render_api_error!(e.message, 400)
end
rescue_from Packages::Composer::ComposerJsonService::InvalidJson do |e|
render_api_error!(e.message, 422)
end
helpers do
def packages
strong_memoize(:packages) do

View File

@ -14,7 +14,6 @@ module Gitlab
end
def details_path
return unless Feature.enabled?(:ci_bridge_pipeline_details, subject.project, default_enabled: true)
return unless can?(user, :read_pipeline, downstream_pipeline)
project_pipeline_path(downstream_project, downstream_pipeline)

View File

@ -4,11 +4,7 @@ module Gitlab
module Database
module Reindexing
def self.perform(index_selector)
Array.wrap(index_selector).each do |index|
ReindexAction.keep_track_of(index) do
ConcurrentReindex.new(index).perform
end
end
Coordinator.new(index_selector).perform
end
def self.candidate_indexes

View File

@ -5,13 +5,13 @@ module Gitlab
module Reindexing
class ConcurrentReindex
include Gitlab::Utils::StrongMemoize
include MigrationHelpers
ReindexError = Class.new(StandardError)
PG_IDENTIFIER_LENGTH = 63
TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
REPLACED_INDEX_PREFIX = 'old_reindex_'
STATEMENT_TIMEOUT = 6.hours
attr_reader :index, :logger
@ -47,7 +47,7 @@ module Gitlab
logger.info("creating replacement index #{replacement_index_name}")
logger.debug("replacement index definition: #{create_replacement_index_statement}")
disable_statement_timeout do
set_statement_timeout do
connection.execute(create_replacement_index_statement)
end
@ -88,7 +88,7 @@ module Gitlab
def remove_index(schema, name)
logger.info("Removing index #{schema}.#{name}")
disable_statement_timeout do
set_statement_timeout do
connection.execute(<<~SQL)
DROP INDEX CONCURRENTLY
IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
@ -110,6 +110,13 @@ module Gitlab
Gitlab::Database::WithLockRetries.new(**arguments).run(raise_on_exhaustion: true, &block)
end
def set_statement_timeout
execute("SET statement_timeout TO #{STATEMENT_TIMEOUT}")
yield
ensure
execute('RESET statement_timeout')
end
delegate :execute, :quote_table_name, to: :connection
def connection
@connection ||= ActiveRecord::Base.connection

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module Database
module Reindexing
class Coordinator
include ExclusiveLeaseGuard
# Maximum lease time for the global Redis lease
# This should be higher than the maximum time for any
# long running step in the reindexing process (compare with
# statement timeouts).
TIMEOUT_PER_ACTION = 1.day
attr_reader :indexes
def initialize(indexes)
@indexes = indexes
end
def perform
indexes.each do |index|
# This obtains a global lease such that there's
# only one live reindexing process at a time.
try_obtain_lease do
ReindexAction.keep_track_of(index) do
ConcurrentReindex.new(index).perform
end
end
end
end
private
def lease_timeout
TIMEOUT_PER_ACTION
end
end
end
end
end

View File

@ -266,7 +266,8 @@ module Gitlab
Gitlab::UsageDataCounters::SourceCodeCounter,
Gitlab::UsageDataCounters::MergeRequestCounter,
Gitlab::UsageDataCounters::DesignsCounter,
Gitlab::UsageDataCounters::KubernetesAgentCounter
Gitlab::UsageDataCounters::KubernetesAgentCounter,
Gitlab::UsageDataCounters::StaticSiteEditorCounter
]
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Gitlab
module UsageDataCounters
class StaticSiteEditorCounter < BaseCounter
KNOWN_EVENTS = %w[views].freeze
PREFIX = 'static_site_editor'
class << self
def increment_views_count
count(:views)
end
end
end
end
end

View File

@ -193,7 +193,7 @@ namespace :gitlab do
end
indexes = if args[:index_name]
Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])
[Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])]
else
Gitlab::Database::Reindexing.candidate_indexes.random_few(2)
end

View File

@ -54,11 +54,16 @@ RSpec.describe Projects::StaticSiteEditorController do
context "as developer" do
before do
allow(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to receive(:increment_views_count)
project.add_role(user, 'developer')
sign_in(user)
get :show, params: default_params
end
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to have_received(:increment_views_count)
end
it 'renders the edit page' do
expect(response).to render_template(:show)
end

View File

@ -1,3 +1,5 @@
import { createUniqueJobId } from '~/pipelines/utils';
export const yamlString = `stages:
- empty
- build
@ -39,18 +41,20 @@ deploy_a:
script: echo hello
`;
const jobId1 = createUniqueJobId('build', 'build_1');
const jobId2 = createUniqueJobId('test', 'test_1');
const jobId3 = createUniqueJobId('test', 'test_2');
const jobId4 = createUniqueJobId('deploy', 'deploy_1');
export const pipelineData = {
stages: [
{
name: 'build',
groups: [],
},
{
name: 'build',
groups: [
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
id: jobId1,
},
],
},
@ -60,10 +64,12 @@ export const pipelineData = {
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
id: jobId2,
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
id: jobId3,
},
],
},
@ -73,8 +79,15 @@ export const pipelineData = {
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy' }],
id: jobId4,
},
],
},
],
jobs: {
[jobId1]: {},
[jobId2]: {},
[jobId3]: {},
[jobId4]: {},
},
};

View File

@ -1,161 +1,211 @@
import { preparePipelineGraphData } from '~/pipelines/utils';
import {
preparePipelineGraphData,
createUniqueJobId,
generateJobNeedsDict,
} from '~/pipelines/utils';
describe('preparePipelineGraphData', () => {
describe('utils functions', () => {
const emptyResponse = { stages: [], jobs: {} };
const jobName1 = 'build_1';
const jobName2 = 'build_2';
const jobName3 = 'test_1';
const jobName4 = 'deploy_1';
const job1 = { [jobName1]: { script: 'echo hello', stage: 'build' } };
const job2 = { [jobName2]: { script: 'echo build', stage: 'build' } };
const job3 = { [jobName3]: { script: 'echo test', stage: 'test' } };
const job4 = { [jobName4]: { script: 'echo deploy', stage: 'deploy' } };
const job1 = { script: 'echo hello', stage: 'build' };
const job2 = { script: 'echo build', stage: 'build' };
const job3 = { script: 'echo test', stage: 'test', needs: [jobName1, jobName2] };
const job4 = { script: 'echo deploy', stage: 'deploy', needs: [jobName3] };
const userDefinedStage = 'myStage';
describe('returns an empty array of stages and empty job objects if', () => {
it('no data is passed', () => {
expect(preparePipelineGraphData({})).toEqual(emptyResponse);
const pipelineGraphData = {
stages: [
{
name: userDefinedStage,
groups: [],
},
{
name: job4.stage,
groups: [
{
name: jobName4,
jobs: [{ ...job4 }],
id: createUniqueJobId(job4.stage, jobName4),
},
],
},
{
name: job1.stage,
groups: [
{
name: jobName1,
jobs: [{ ...job1 }],
id: createUniqueJobId(job1.stage, jobName1),
},
{
name: jobName2,
jobs: [{ ...job2 }],
id: createUniqueJobId(job2.stage, jobName2),
},
],
},
{
name: job3.stage,
groups: [
{
name: jobName3,
jobs: [{ ...job3 }],
id: createUniqueJobId(job3.stage, jobName3),
},
],
},
],
jobs: {
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
[jobName3]: { ...job3, id: createUniqueJobId(job3.stage, jobName3) },
[jobName4]: { ...job4, id: createUniqueJobId(job4.stage, jobName4) },
},
};
describe('preparePipelineGraphData', () => {
describe('returns an empty array of stages and empty job objects if', () => {
it('no data is passed', () => {
expect(preparePipelineGraphData({})).toEqual(emptyResponse);
});
it('no stages are found', () => {
expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual(
emptyResponse,
);
});
});
it('no stages are found', () => {
expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual(
emptyResponse,
);
describe('returns the correct array of stages and object of jobs', () => {
it('when multiple jobs are in the same stage', () => {
const expectedData = {
stages: [
{
name: job1.stage,
groups: [
{
name: jobName1,
jobs: [{ ...job1 }],
id: createUniqueJobId(job1.stage, jobName1),
},
{
name: jobName2,
jobs: [{ ...job2 }],
id: createUniqueJobId(job2.stage, jobName2),
},
],
},
],
jobs: {
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
},
};
expect(
preparePipelineGraphData({ [jobName1]: { ...job1 }, [jobName2]: { ...job2 } }),
).toEqual(expectedData);
});
it('when stages are defined by the user', () => {
const userDefinedStage2 = 'myStage2';
const expectedData = {
stages: [
{
name: userDefinedStage,
groups: [],
},
{
name: userDefinedStage2,
groups: [],
},
],
jobs: {},
};
expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual(
expectedData,
);
});
it('by combining user defined stage and job stages, it preserves user defined order', () => {
const userDefinedStageThatOverlaps = 'deploy';
expect(
preparePipelineGraphData({
stages: [userDefinedStage, userDefinedStageThatOverlaps],
[jobName1]: { ...job1 },
[jobName2]: { ...job2 },
[jobName3]: { ...job3 },
[jobName4]: { ...job4 },
}),
).toEqual(pipelineGraphData);
});
it('with only unique values', () => {
const expectedData = {
stages: [
{
name: job1.stage,
groups: [
{
name: jobName1,
jobs: [{ ...job1 }],
id: createUniqueJobId(job1.stage, jobName1),
},
],
},
],
jobs: {
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
},
};
expect(
preparePipelineGraphData({
stages: ['build'],
[jobName1]: { ...job1 },
[jobName1]: { ...job1 },
}),
).toEqual(expectedData);
});
});
});
describe('returns the correct array of stages and object of jobs', () => {
it('when multiple jobs are in the same stage', () => {
const expectedData = {
stages: [
{
name: job1[jobName1].stage,
groups: [
{
name: jobName1,
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
},
{
name: jobName2,
jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
},
],
},
],
jobs: { ...job1, ...job2 },
};
expect(preparePipelineGraphData({ ...job1, ...job2 })).toEqual(expectedData);
describe('generateJobNeedsDict', () => {
it('generates an empty object if it receives no jobs', () => {
expect(generateJobNeedsDict({ jobs: {} })).toEqual({});
});
it('when stages are defined by the user', () => {
const userDefinedStage = 'myStage';
const userDefinedStage2 = 'myStage2';
const expectedData = {
stages: [
{
name: userDefinedStage,
groups: [],
},
{
name: userDefinedStage2,
groups: [],
},
],
jobs: {},
};
expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual(
expectedData,
);
});
it('by combining user defined stage and job stages, it preserves user defined order', () => {
const userDefinedStage = 'myStage';
const userDefinedStageThatOverlaps = 'deploy';
const expectedData = {
stages: [
{
name: userDefinedStage,
groups: [],
},
{
name: job4[jobName4].stage,
groups: [
{
name: jobName4,
jobs: [{ script: job4[jobName4].script, stage: job4[jobName4].stage }],
},
],
},
{
name: job1[jobName1].stage,
groups: [
{
name: jobName1,
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
},
{
name: jobName2,
jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
},
],
},
{
name: job3[jobName3].stage,
groups: [
{
name: jobName3,
jobs: [{ script: job3[jobName3].script, stage: job3[jobName3].stage }],
},
],
},
],
it('generates a dict with empty needs if there are no dependencies', () => {
const smallGraph = {
jobs: {
...job1,
...job2,
...job3,
...job4,
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
},
};
expect(
preparePipelineGraphData({
stages: [userDefinedStage, userDefinedStageThatOverlaps],
...job1,
...job2,
...job3,
...job4,
}),
).toEqual(expectedData);
expect(generateJobNeedsDict(smallGraph)).toEqual({
[pipelineGraphData.jobs[jobName1].id]: [],
[pipelineGraphData.jobs[jobName2].id]: [],
});
});
it('with only unique values', () => {
const expectedData = {
stages: [
{
name: job1[jobName1].stage,
groups: [
{
name: jobName1,
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
},
],
},
],
jobs: {
...job1,
},
};
it('generates a dict where key is the a job and its value is an array of all its needs', () => {
const uniqueJobName1 = pipelineGraphData.jobs[jobName1].id;
const uniqueJobName2 = pipelineGraphData.jobs[jobName2].id;
const uniqueJobName3 = pipelineGraphData.jobs[jobName3].id;
const uniqueJobName4 = pipelineGraphData.jobs[jobName4].id;
expect(
preparePipelineGraphData({
stages: ['build'],
...job1,
...job1,
}),
).toEqual(expectedData);
expect(generateJobNeedsDict(pipelineGraphData)).toEqual({
[uniqueJobName1]: [],
[uniqueJobName2]: [],
[uniqueJobName3]: [uniqueJobName1, uniqueJobName2],
[uniqueJobName4]: [uniqueJobName3, uniqueJobName1, uniqueJobName2],
});
});
});
});

View File

@ -13,7 +13,8 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
value="foo/bar/test.md"
/>
<blob-content-edit-stub
<editor-lite-stub
editoroptions="[object Object]"
fileglobalid="blob_local_7"
filename="foo/bar/test.md"
value="Lorem ipsum dolar sit amet,

View File

@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as createFlash } from '~/flash';
@ -48,7 +48,7 @@ describe('Snippet Blob Edit component', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findHeader = () => wrapper.find(BlobHeaderEdit);
const findContent = () => wrapper.find(BlobContentEdit);
const findContent = () => wrapper.find(EditorLite);
const getLastUpdatedArgs = () => {
const event = wrapper.emitted()['blob-updated'];
@ -172,11 +172,13 @@ describe('Snippet Blob Edit component', () => {
expect(findContent().exists()).toBe(showContent);
if (showContent) {
expect(findContent().props()).toEqual({
value: TEST_BLOB_LOADED.content,
fileGlobalId: TEST_BLOB_LOADED.id,
fileName: TEST_BLOB_LOADED.path,
});
expect(findContent().props()).toEqual(
expect.objectContaining({
value: TEST_BLOB_LOADED.content,
fileGlobalId: TEST_BLOB_LOADED.id,
fileName: TEST_BLOB_LOADED.path,
}),
);
}
});
});

View File

@ -28,7 +28,7 @@ describe('Tracking', () => {
respectDoNotTrack: true,
forceSecureTracker: true,
eventMethod: 'post',
contexts: { webPage: true },
contexts: { webPage: true, performanceTiming: true },
formTracking: false,
linkClickTracking: false,
});

View File

@ -38,4 +38,40 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
describe '#diff_stats_summary' do
subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
let(:current_user) { create :admin }
let(:query) do
%(
{
project(fullPath: "#{project.full_path}") {
mergeRequests {
nodes {
diffStatsSummary {
additions, deletions
}
}
}
}
}
)
end
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
let(:response) { subject.dig('data', 'project', 'mergeRequests', 'nodes').first['diffStatsSummary'] }
context 'when MR metrics has additions and deletions' do
before do
merge_request.metrics.update!(added_lines: 5, removed_lines: 8)
end
it 'pulls out data from metrics object' do
expect(response).to match('additions' => 5, 'deletions' => 8)
end
end
end
end

View File

@ -30,15 +30,6 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
it { expect(subject).to have_details }
it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" }
context 'when ci_bridge_pipeline_details is disabled' do
before do
stub_feature_flags(ci_bridge_pipeline_details: false)
end
it { expect(subject).not_to have_details }
it { expect(subject.details_path).to be_nil }
end
end
context 'when user does not have access to read downstream pipeline' do

View File

@ -107,11 +107,11 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
context 'mocked specs' do
before do
allow(subject).to receive(:connection).and_return(connection)
allow(subject).to receive(:disable_statement_timeout).and_yield
allow(connection).to receive(:execute).and_call_original
end
it 'replaces the existing index with an identical index' do
expect(subject).to receive(:disable_statement_timeout).twice.and_yield
expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').twice
expect_to_execute_concurrently_in_order(create_index)
@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
end
it 'replaces the existing index with an identical index' do
expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').exactly(3).times
expect_to_execute_concurrently_in_order(drop_index)
expect_to_execute_concurrently_in_order(create_index)

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Reindexing::Coordinator do
include ExclusiveLeaseHelpers
describe '.perform' do
subject { described_class.new(indexes).perform }
let(:indexes) { [instance_double(Gitlab::Database::PostgresIndex), instance_double(Gitlab::Database::PostgresIndex)] }
let(:reindexers) { [instance_double(Gitlab::Database::Reindexing::ConcurrentReindex), instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)] }
let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
let(:lease_key) { 'gitlab/database/reindexing/coordinator' }
let(:lease_timeout) { 1.day }
let(:uuid) { 'uuid' }
before do
allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield
indexes.zip(reindexers).each do |index, reindexer|
allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer)
allow(reindexer).to receive(:perform)
end
end
it 'performs concurrent reindexing for each index' do
indexes.zip(reindexers).each do |index, reindexer|
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer)
expect(reindexer).to receive(:perform)
end
subject
end
it 'keeps track of actions and creates ReindexAction records' do
indexes.each do |index|
expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield
end
subject
end
context 'locking' do
it 'acquires a lock while reindexing' do
indexes.each do |index|
expect(lease).to receive(:try_obtain).ordered.and_return(uuid)
action = instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).ordered.with(index).and_return(action)
expect(action).to receive(:perform).ordered
expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid)
end
subject
end
it 'does does not perform reindexing actions if lease is not granted' do
indexes.each do |index|
expect(lease).to receive(:try_obtain).ordered.and_return(false)
expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new)
end
subject
end
end
end
end

View File

@ -3,53 +3,19 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Reindexing do
include ExclusiveLeaseHelpers
describe '.perform' do
before do
allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield
end
subject { described_class.perform(indexes) }
shared_examples_for 'reindexing' do
before do
indexes.zip(reindexers).each do |index, reindexer|
allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer)
allow(reindexer).to receive(:perform)
end
end
let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
let(:indexes) { double }
it 'performs concurrent reindexing for each index' do
indexes.zip(reindexers).each do |index, reindexer|
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer)
expect(reindexer).to receive(:perform)
end
it 'delegates to Coordinator' do
expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator)
expect(coordinator).to receive(:perform)
subject
end
it 'keeps track of actions and creates ReindexAction records' do
indexes.each do |index|
expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield
end
subject
end
end
context 'with multiple indexes' do
subject { described_class.perform(indexes) }
let(:indexes) { [instance_double('Gitlab::Database::PostgresIndex'), instance_double('Gitlab::Database::PostgresIndex')] }
let(:reindexers) { [instance_double('Gitlab::Database::Reindexing::ConcurrentReindex'), instance_double('Gitlab::Database::Reindexing::ConcurrentReindex')] }
it_behaves_like 'reindexing'
end
context 'single index' do
subject { described_class.perform(indexes.first) }
let(:indexes) { [instance_double('Gitlab::Database::PostgresIndex')] }
let(:reindexers) { [instance_double('Gitlab::Database::Reindexing::ConcurrentReindex')] }
it_behaves_like 'reindexing'
subject
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::UsageDataCounters::StaticSiteEditorCounter do
it_behaves_like 'a redis usage counter', 'StaticSiteEditor', :views
it_behaves_like 'a redis usage counter with totals', :static_site_editor,
views: 3
end

View File

@ -618,6 +618,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.usage_counters }
it { is_expected.to include(:kubernetes_agent_gitops_sync) }
it { is_expected.to include(:static_site_editor_views) }
end
describe '.usage_data_counters' do

View File

@ -289,6 +289,34 @@ RSpec.describe API::ComposerPackages do
it_behaves_like 'process Composer api request', :developer, :not_found
end
end
context 'with invalid composer.json' do
let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
let(:params) { { tag: 'v1.2.99' } }
let(:project) { create(:project, :custom_repo, files: files, group: group) }
before do
project.repository.add_tag(user, 'v1.2.99', 'master')
end
context 'with a missing composer.json file' do
let(:files) { { 'some_other_file' => '' } }
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
end
context 'with an empty composer.json file' do
let(:files) { { 'composer.json' => '' } }
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
end
context 'with a malformed composer.json file' do
let(:files) { { 'composer.json' => 'not_valid_JSON' } }
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
end
end
end
describe 'GET /api/v4/projects/:id/packages/composer/archives/*package_name?sha=:sha' do

View File

@ -45,6 +45,10 @@ RSpec.describe Jira::Requests::Projects::ListService do
end
it 'returns an error response' do
expect(Gitlab::ProjectServiceLogger).to receive(:error).with(
hash_including(
error: hash_including(:exception_class, :exception_message, :exception_backtrace)))
.and_call_original
expect(subject.error?).to be_truthy
expect(subject.message).to eq('Jira request error: Timeout::Error')
end

View File

@ -23,7 +23,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
let(:json) { '{ name": "package-name"}' }
it 'raises an error' do
expect { subject }.to raise_error(/Invalid/)
expect { subject }.to raise_error(described_class::InvalidJson, /Invalid/)
end
end
end
@ -32,7 +32,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
let(:project) { create(:project, :repository) }
it 'raises an error' do
expect { subject }.to raise_error(/not found/)
expect { subject }.to raise_error(described_class::InvalidJson, /not found/)
end
end
end

View File

@ -248,7 +248,7 @@ RSpec.describe 'gitlab:db namespace rake task' do
it 'calls the index rebuilder with the proper arguments' do
expect(Gitlab::Database::PostgresIndex).to receive(:by_identifier).with('public.foo_idx').and_return(index)
expect(Gitlab::Database::Reindexing).to receive(:perform).with(index)
expect(Gitlab::Database::Reindexing).to receive(:perform).with([index])
run_rake_task('gitlab:db:reindex', '[public.foo_idx]')
end