Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0b679004a6
commit
0cb47d7129
59 changed files with 2342 additions and 149 deletions
|
@ -42,6 +42,7 @@ const Api = {
|
|||
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
|
||||
projectRunnersPath: '/api/:version/projects/:id/runners',
|
||||
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
|
||||
projectProtectedBranchesNamePath: '/api/:version/projects/:id/protected_branches/:name',
|
||||
projectSearchPath: '/api/:version/projects/:id/search',
|
||||
projectSharePath: '/api/:version/projects/:id/share',
|
||||
projectMilestonesPath: '/api/:version/projects/:id/milestones',
|
||||
|
@ -372,6 +373,14 @@ const Api = {
|
|||
.then(({ data }) => data);
|
||||
},
|
||||
|
||||
projectProtectedBranch(id, branchName) {
|
||||
const url = Api.buildUrl(Api.projectProtectedBranchesNamePath)
|
||||
.replace(':id', encodeURIComponent(id))
|
||||
.replace(':name', branchName);
|
||||
|
||||
return axios.get(url).then(({ data }) => data);
|
||||
},
|
||||
|
||||
projectSearch(id, options = {}) {
|
||||
const url = Api.buildUrl(Api.projectSearchPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
|
|
|
@ -19,3 +19,7 @@ export function loadCSSFile(path) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getCssVariable(variable) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
|
||||
import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors';
|
||||
|
||||
initProfilePreferences();
|
||||
initProfilePreferencesDiffsColors();
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<script>
|
||||
import { validateHexColor, hexToRgb } from '~/lib/utils/color_utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { getCssVariable } from '~/lib/utils/css_utils';
|
||||
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
|
||||
import DiffsColorsPreview from './diffs_colors_preview.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColorPicker,
|
||||
DiffsColorsPreview,
|
||||
},
|
||||
inject: ['deletion', 'addition'],
|
||||
data() {
|
||||
return {
|
||||
deletionColor: this.deletion || '',
|
||||
additionColor: this.addition || '',
|
||||
defaultDeletionColor: getCssVariable('--default-diff-color-deletion'),
|
||||
defaultAdditionColor: getCssVariable('--default-diff-color-addition'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
suggestedColors() {
|
||||
const colors = {
|
||||
'#d99530': s__('SuggestedColors|Orange'),
|
||||
'#1f75cb': s__('SuggestedColors|Blue'),
|
||||
};
|
||||
if (this.isValidColor(this.deletion)) {
|
||||
colors[this.deletion] = s__('SuggestedColors|Current removal color');
|
||||
}
|
||||
if (this.isValidColor(this.addition)) {
|
||||
colors[this.addition] = s__('SuggestedColors|Current addition color');
|
||||
}
|
||||
if (this.isValidColor(this.defaultDeletionColor)) {
|
||||
colors[this.defaultDeletionColor] = s__('SuggestedColors|Default removal color');
|
||||
}
|
||||
if (this.isValidColor(this.defaultAdditionColor)) {
|
||||
colors[this.defaultAdditionColor] = s__('SuggestedColors|Default addition color');
|
||||
}
|
||||
return colors;
|
||||
},
|
||||
previewClasses() {
|
||||
return {
|
||||
'diff-custom-addition-color': this.isValidColor(this.additionColor),
|
||||
'diff-custom-deletion-color': this.isValidColor(this.deletionColor),
|
||||
};
|
||||
},
|
||||
previewStyle() {
|
||||
let style = {};
|
||||
if (this.isValidColor(this.deletionColor)) {
|
||||
const colorRgb = hexToRgb(this.deletionColor).join();
|
||||
style = {
|
||||
...style,
|
||||
'--diff-deletion-color': `rgba(${colorRgb},0.2)`,
|
||||
};
|
||||
}
|
||||
if (this.isValidColor(this.additionColor)) {
|
||||
const colorRgb = hexToRgb(this.additionColor).join();
|
||||
style = {
|
||||
...style,
|
||||
'--diff-addition-color': `rgba(${colorRgb},0.2)`,
|
||||
};
|
||||
}
|
||||
return style;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isValidColor(color) {
|
||||
return validateHexColor(color);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
colorDeletionInputLabel: s__('Preferences|Color for removed lines'),
|
||||
colorAdditionInputLabel: s__('Preferences|Color for added lines'),
|
||||
previewLabel: s__('Preferences|Preview'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div :style="previewStyle" :class="previewClasses">
|
||||
<diffs-colors-preview />
|
||||
<color-picker
|
||||
v-model="deletionColor"
|
||||
:label="$options.i18n.colorDeletionInputLabel"
|
||||
:state="isValidColor(deletionColor)"
|
||||
:suggested-colors="suggestedColors"
|
||||
/>
|
||||
<input
|
||||
id="user_diffs_deletion_color"
|
||||
v-model="deletionColor"
|
||||
name="user[diffs_deletion_color]"
|
||||
type="hidden"
|
||||
/>
|
||||
<color-picker
|
||||
v-model="additionColor"
|
||||
:label="$options.i18n.colorAdditionInputLabel"
|
||||
:state="isValidColor(additionColor)"
|
||||
:suggested-colors="suggestedColors"
|
||||
/>
|
||||
<input
|
||||
id="user_diffs_addition_color"
|
||||
v-model="additionColor"
|
||||
name="user[diffs_addition_color]"
|
||||
type="hidden"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,231 @@
|
|||
<script>
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
themeClass() {
|
||||
return window.gon?.user_color_scheme;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
previewLabel: s__('Preferences|Preview'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label>{{ $options.i18n.previewLabel }}</label>
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
|
||||
<table :class="themeClass" class="code">
|
||||
<tbody>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="1"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span
|
||||
><span class="c1"># <span class="idiff deletion">Removed</span> content</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="1"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span
|
||||
><span class="c1"># <span class="idiff addition">Added</span> content</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="2"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span><span class="n">v</span> <span class="o">=</span> <span class="mi">1</span></span>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="2"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span><span class="n">v</span> <span class="o">=</span> <span class="mi">1</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="3"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span
|
||||
><span class="n">s</span> <span class="o">=</span>
|
||||
<span class="s">"string"</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="3"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span
|
||||
><span class="n">s</span> <span class="o">=</span>
|
||||
<span class="s">"string"</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="4"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old"><span></span></td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="4"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new"><span></span></td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="5"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span
|
||||
><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span>
|
||||
<span class="nb">range</span><span class="p">(</span><span class="o">-</span
|
||||
><span class="mi">10</span><span class="p">,</span> <span class="mi">10</span
|
||||
><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="5"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span
|
||||
><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span>
|
||||
<span class="nb">range</span><span class="p">(</span><span class="o">-</span
|
||||
><span class="mi">10</span><span class="p">,</span> <span class="mi">10</span
|
||||
><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="6"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="k">print</span><span class="p">(</span><span class="n">i</span>
|
||||
<span class="o">+</span> <span class="mi">1</span><span class="p">)</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="6"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="k">print</span><span class="p">(</span><span class="n">i</span>
|
||||
<span class="o">+</span> <span class="mi">1</span><span class="p">)</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="7"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old"><span></span></td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="7"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new"><span></span></td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="8"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span
|
||||
><span class="k">class</span> <span class="nc">LinkedList</span
|
||||
><span class="p">(</span><span class="nb">object</span><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="8"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span
|
||||
><span class="k">class</span> <span class="nc">LinkedList</span
|
||||
><span class="p">(</span><span class="nb">object</span><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="9"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span
|
||||
><span class="bp">self</span><span class="p">,</span> <span class="n">x</span
|
||||
><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="9"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span
|
||||
><span class="bp">self</span><span class="p">,</span> <span class="n">x</span
|
||||
><span class="p">):</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="10"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="bp">self</span><span class="p">.</span><span class="n">val</span>
|
||||
<span class="o">=</span> <span class="n">x</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="10"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="bp">self</span><span class="p">.</span><span class="n">val</span>
|
||||
<span class="o">=</span> <span class="n">x</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="line_holder parallel">
|
||||
<td class="old_line diff-line-num old">
|
||||
<a data-linenumber="11"></a>
|
||||
</td>
|
||||
<td class="line_content parallel left-side old">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="bp">self</span><span class="p">.</span><span class="nb">next</span>
|
||||
<span class="o">=</span> <span class="bp">None</span></span
|
||||
>
|
||||
</td>
|
||||
<td class="new_line diff-line-num new">
|
||||
<a data-linenumber="11"></a>
|
||||
</td>
|
||||
<td class="line_content parallel right-side new">
|
||||
<span>
|
||||
<span>{{ ' ' }}</span>
|
||||
<span class="bp">self</span><span class="p">.</span><span class="nb">next</span>
|
||||
<span class="o">=</span> <span class="bp">None</span></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
|
||||
</div>
|
||||
</template>
|
|
@ -45,7 +45,7 @@ export default {
|
|||
return {
|
||||
isSubmitEnabled: true,
|
||||
darkModeOnCreate: null,
|
||||
darkModeOnSubmit: null,
|
||||
schemeOnCreate: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -61,6 +61,7 @@ export default {
|
|||
this.formEl.addEventListener('ajax:success', this.handleSuccess);
|
||||
this.formEl.addEventListener('ajax:error', this.handleError);
|
||||
this.darkModeOnCreate = this.darkModeSelected();
|
||||
this.schemeOnCreate = this.getSelectedScheme();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.formEl.removeEventListener('ajax:beforeSend', this.handleLoading);
|
||||
|
@ -76,15 +77,19 @@ export default {
|
|||
const themeId = new FormData(this.formEl).get('user[theme_id]');
|
||||
return this.applicationThemes[themeId] ?? null;
|
||||
},
|
||||
getSelectedScheme() {
|
||||
return new FormData(this.formEl).get('user[color_scheme_id]');
|
||||
},
|
||||
handleLoading() {
|
||||
this.isSubmitEnabled = false;
|
||||
this.darkModeOnSubmit = this.darkModeSelected();
|
||||
},
|
||||
handleSuccess(customEvent) {
|
||||
// Reload the page if the theme has changed from light to dark mode or vice versa
|
||||
// to correctly load all required styles.
|
||||
const modeChanged = this.darkModeOnCreate ? !this.darkModeOnSubmit : this.darkModeOnSubmit;
|
||||
if (modeChanged) {
|
||||
// or if color scheme has changed to correctly load all required styles.
|
||||
if (
|
||||
this.darkModeOnCreate !== this.darkModeSelected() ||
|
||||
this.schemeOnCreate !== this.getSelectedScheme()
|
||||
) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import Vue from 'vue';
|
||||
import DiffsColors from './components/diffs_colors.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('#js-profile-preferences-diffs-colors-app');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
const { deletion, addition } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
deletion,
|
||||
addition,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(DiffsColors);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* CSS variables used below are declared in `app/views/layouts/_diffs_colors_css.haml`
|
||||
*/
|
||||
.diff-custom-addition-color {
|
||||
.code {
|
||||
.line_holder {
|
||||
.diff-line-num,
|
||||
.line-coverage,
|
||||
.line-codequality,
|
||||
.line_content {
|
||||
&.new {
|
||||
&:not(.hll) {
|
||||
background: var(--diff-addition-color);
|
||||
}
|
||||
|
||||
&.line_content span.idiff {
|
||||
background: var(--diff-addition-color) !important;
|
||||
}
|
||||
|
||||
&::before,
|
||||
a {
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gd {
|
||||
background-color: var(--diff-addition-color);
|
||||
}
|
||||
}
|
||||
|
||||
.idiff.addition {
|
||||
background: var(--diff-addition-color) !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* CSS variables used below are declared in `app/views/layouts/_diffs_colors_css.haml`
|
||||
*/
|
||||
.diff-custom-deletion-color {
|
||||
.code {
|
||||
.line_holder {
|
||||
.diff-line-num,
|
||||
.line-coverage,
|
||||
.line-codequality,
|
||||
.line_content {
|
||||
&.old {
|
||||
&:not(.hll) {
|
||||
background: var(--diff-deletion-color);
|
||||
}
|
||||
|
||||
&.line_content span.idiff {
|
||||
background: var(--diff-deletion-color) !important;
|
||||
}
|
||||
|
||||
&::before,
|
||||
a {
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gd {
|
||||
background-color: var(--diff-deletion-color);
|
||||
}
|
||||
}
|
||||
|
||||
.idiff.deletion {
|
||||
background: var(--diff-deletion-color) !important;
|
||||
}
|
||||
}
|
|
@ -120,6 +120,8 @@ $dark-il: #de935f;
|
|||
--color-hljs-selector-id: #{$dark-nn};
|
||||
--color-hljs-selector-attr: #{$dark-nt};
|
||||
--color-hljs-selector-pseudo: #{$dark-nd};
|
||||
--default-diff-color-deletion: #ff3333;
|
||||
--default-diff-color-addition: #288f2a;
|
||||
}
|
||||
|
||||
.code.dark {
|
||||
|
|
|
@ -89,6 +89,11 @@ $monokai-gd: #f92672;
|
|||
$monokai-gi: #a6e22e;
|
||||
$monokai-gh: #75715e;
|
||||
|
||||
:root {
|
||||
--default-diff-color-deletion: #c87872;
|
||||
--default-diff-color-addition: #678528;
|
||||
}
|
||||
|
||||
.code.monokai {
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
|
|
|
@ -9,6 +9,11 @@
|
|||
background-color: $white-normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
--default-diff-color-deletion: #b4b4b4;
|
||||
--default-diff-color-addition: #b4b4b4;
|
||||
}
|
||||
|
||||
.code.none {
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
|
|
|
@ -92,6 +92,11 @@ $solarized-dark-vg: #268bd2;
|
|||
$solarized-dark-vi: #268bd2;
|
||||
$solarized-dark-il: #2aa198;
|
||||
|
||||
:root {
|
||||
--default-diff-color-deletion: #ff362c;
|
||||
--default-diff-color-addition: #647e0e;
|
||||
}
|
||||
|
||||
.code.solarized-dark {
|
||||
// Line numbers
|
||||
.file-line-num {
|
||||
|
|
|
@ -94,6 +94,11 @@ $solarized-light-vg: #268bd2;
|
|||
$solarized-light-vi: #268bd2;
|
||||
$solarized-light-il: #2aa198;
|
||||
|
||||
:root {
|
||||
--default-diff-color-deletion: #dc322f;
|
||||
--default-diff-color-addition: #859900;
|
||||
}
|
||||
|
||||
@mixin match-line {
|
||||
color: $black-transparent;
|
||||
background: $solarized-light-matchline-bg;
|
||||
|
|
|
@ -3,3 +3,8 @@
|
|||
|
||||
@include conflict-colors('white');
|
||||
}
|
||||
|
||||
:root {
|
||||
--default-diff-color-deletion: #eb919b;
|
||||
--default-diff-color-addition: #a0f5b4;
|
||||
}
|
|
@ -149,7 +149,6 @@ pre.code,
|
|||
.diff-line-num {
|
||||
&.old {
|
||||
background-color: $line-number-old;
|
||||
border-color: $line-removed-dark;
|
||||
|
||||
a {
|
||||
color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%);
|
||||
|
@ -158,7 +157,6 @@ pre.code,
|
|||
|
||||
&.new {
|
||||
background-color: $line-number-new;
|
||||
border-color: $line-added-dark;
|
||||
|
||||
a {
|
||||
color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%);
|
||||
|
|
|
@ -36,6 +36,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
|||
def preferences_param_names
|
||||
[
|
||||
:color_scheme_id,
|
||||
:diffs_deletion_color,
|
||||
:diffs_addition_color,
|
||||
:layout,
|
||||
:dashboard,
|
||||
:project_view,
|
||||
|
|
23
app/helpers/colors_helper.rb
Normal file
23
app/helpers/colors_helper.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ColorsHelper
|
||||
HEX_COLOR_PATTERN = /\A\#(?:[0-9A-Fa-f]{3}){1,2}\Z/.freeze
|
||||
|
||||
def hex_color_to_rgb_array(hex_color)
|
||||
raise ArgumentError, "invalid hex color `#{hex_color}`" unless hex_color =~ HEX_COLOR_PATTERN
|
||||
|
||||
hex_color.length == 7 ? hex_color[1, 7].scan(/.{2}/).map(&:hex) : hex_color[1, 4].scan(/./).map { |v| (v * 2).hex }
|
||||
end
|
||||
|
||||
def rgb_array_to_hex_color(rgb_array)
|
||||
raise ArgumentError, "invalid RGB array `#{rgb_array}`" unless rgb_array_valid?(rgb_array)
|
||||
|
||||
"##{rgb_array.map{ "%02x" % _1 }.join}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rgb_array_valid?(rgb_array)
|
||||
rgb_array.is_a?(Array) && rgb_array.length == 3 && rgb_array.all?{ _1 >= 0 && _1 <= 255 }
|
||||
end
|
||||
end
|
|
@ -82,6 +82,22 @@ module PreferencesHelper
|
|||
Gitlab::TabWidth.css_class_for_user(current_user)
|
||||
end
|
||||
|
||||
def user_diffs_colors
|
||||
{
|
||||
deletion: current_user&.diffs_deletion_color.presence,
|
||||
addition: current_user&.diffs_addition_color.presence
|
||||
}.compact
|
||||
end
|
||||
|
||||
def custom_diff_color_classes
|
||||
return if request.path == profile_preferences_path
|
||||
|
||||
classes = []
|
||||
classes << 'diff-custom-addition-color' if current_user&.diffs_addition_color.presence
|
||||
classes << 'diff-custom-deletion-color' if current_user&.diffs_deletion_color.presence
|
||||
classes
|
||||
end
|
||||
|
||||
def language_choices
|
||||
options_for_select(
|
||||
selectable_locales_with_translation_level.sort,
|
||||
|
|
|
@ -324,6 +324,8 @@ class User < ApplicationRecord
|
|||
:setup_for_company, :setup_for_company=,
|
||||
:render_whitespace_in_code, :render_whitespace_in_code=,
|
||||
:markdown_surround_selection, :markdown_surround_selection=,
|
||||
:diffs_deletion_color, :diffs_deletion_color=,
|
||||
:diffs_addition_color, :diffs_addition_color=,
|
||||
to: :user_preference
|
||||
|
||||
delegate :path, to: :namespace, allow_nil: true, prefix: true
|
||||
|
|
|
@ -19,6 +19,9 @@ class UserPreference < ApplicationRecord
|
|||
greater_than_or_equal_to: Gitlab::TabWidth::MIN,
|
||||
less_than_or_equal_to: Gitlab::TabWidth::MAX
|
||||
}
|
||||
validates :diffs_deletion_color, :diffs_addition_color,
|
||||
format: { with: ColorsHelper::HEX_COLOR_PATTERN },
|
||||
allow_blank: true
|
||||
|
||||
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
.col-sm-2.col-form-label.gl-pt-0
|
||||
= f.label :can_create_group
|
||||
.col-sm-10
|
||||
= f.check_box :can_create_group
|
||||
= f.gitlab_ui_checkbox_component :can_create_group, ''
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
|
@ -39,10 +39,7 @@
|
|||
= f.label :external
|
||||
.hidden{ data: user_internal_regex_data }
|
||||
.col-sm-10.gl-display-flex.gl-align-items-baseline
|
||||
= f.check_box :external do
|
||||
= s_('AdminUsers|External')
|
||||
%p.light.gl-pl-2
|
||||
= s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
|
||||
= f.gitlab_ui_checkbox_component :external, s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
|
||||
%row.hidden#warning_external_automatically_set
|
||||
= gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
|
||||
|
||||
|
@ -50,12 +47,9 @@
|
|||
- @user.credit_card_validation || @user.build_credit_card_validation
|
||||
= f.fields_for :credit_card_validation do |ff|
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
= ff.label s_("AdminUsers|Validate user account")
|
||||
= ff.label s_('AdminUsers|Validate user account')
|
||||
.col-sm-10.gl-display-flex.gl-align-items-baseline
|
||||
= ff.check_box :credit_card_validated_at, checked: @user.credit_card_validated_at.present?
|
||||
.gl-pl-2
|
||||
.light
|
||||
= s_('AdminUsers|User is validated and can use free CI minutes on shared runners.')
|
||||
.gl-text-gray-600
|
||||
= s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user.')
|
||||
|
||||
= ff.gitlab_ui_checkbox_component :credit_card_validated_at,
|
||||
s_('AdminUsers|User is validated and can use free CI minutes on shared runners.'),
|
||||
help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user.'),
|
||||
checkbox_options: { checked: @user.credit_card_validated_at.present? }
|
||||
|
|
20
app/views/layouts/_diffs_colors_css.haml
Normal file
20
app/views/layouts/_diffs_colors_css.haml
Normal file
|
@ -0,0 +1,20 @@
|
|||
- deletion_color = local_assigns.fetch(:deletion, nil)
|
||||
- addition_color = local_assigns.fetch(:addition, nil)
|
||||
|
||||
- if deletion_color.present? || request.path == profile_preferences_path
|
||||
= stylesheet_link_tag_defer "highlight/diff_custom_colors_deletion"
|
||||
- if deletion_color.present?
|
||||
- deletion_color_rgb = hex_color_to_rgb_array(deletion_color).join(',')
|
||||
:css
|
||||
:root {
|
||||
--diff-deletion-color: rgba(#{deletion_color_rgb},0.2);
|
||||
}
|
||||
|
||||
- if addition_color.present? || request.path == profile_preferences_path
|
||||
= stylesheet_link_tag_defer "highlight/diff_custom_colors_addition"
|
||||
- if addition_color.present?
|
||||
- addition_color_rgb = hex_color_to_rgb_array(addition_color).join(',')
|
||||
:css
|
||||
:root {
|
||||
--diff-addition-color: rgba(#{addition_color_rgb},0.2);
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
- startup_filename_default = user_application_theme == 'gl-dark' ? 'dark' : 'general'
|
||||
- startup_filename = local_assigns.fetch(:startup_filename, nil) || startup_filename_default
|
||||
- diffs_colors = user_diffs_colors
|
||||
|
||||
%style
|
||||
= Rails.application.assets_manifest.find_sources("themes/#{user_application_theme_css_filename}.css").first.to_s.html_safe if user_application_theme_css_filename
|
||||
= Rails.application.assets_manifest.find_sources("startup/startup-#{startup_filename}.css").first.to_s.html_safe
|
||||
|
||||
= render 'layouts/diffs_colors_css', diffs_colors if diffs_colors.present? || request.path == profile_preferences_path
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- page_classes = page_class << @html_class
|
||||
- page_classes = page_classes.flatten.compact
|
||||
- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list]
|
||||
- body_classes = [user_application_theme, user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
|
||||
|
||||
!!! 5
|
||||
%html{ lang: I18n.locale, class: page_classes }
|
||||
|
|
|
@ -44,6 +44,19 @@
|
|||
.col-sm-12
|
||||
%hr
|
||||
|
||||
.row.js-preferences-form.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar#diffs-colors
|
||||
%h4.gl-mt-0
|
||||
= s_('Preferences|Diff colors')
|
||||
%p
|
||||
= s_('Preferences|Customize the colors of removed and added lines in diffs.')
|
||||
.col-lg-8
|
||||
.form-group
|
||||
#js-profile-preferences-diffs-colors-app{ data: user_diffs_colors }
|
||||
|
||||
.col-sm-12
|
||||
%hr
|
||||
|
||||
.row.js-preferences-form.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar#behavior
|
||||
%h4.gl-mt-0
|
||||
|
|
|
@ -314,6 +314,8 @@ module Gitlab
|
|||
config.assets.precompile << "themes/*.css"
|
||||
|
||||
config.assets.precompile << "highlight/themes/*.css"
|
||||
config.assets.precompile << "highlight/diff_custom_colors_addition.css"
|
||||
config.assets.precompile << "highlight/diff_custom_colors_deletion.css"
|
||||
|
||||
# Import gitlab-svgs directly from vendored directory
|
||||
config.assets.paths << "#{config.root}/node_modules/@gitlab/svgs/dist"
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: vulnerability_report_page_size_selector
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82438
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356888
|
||||
milestone: '14.10'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351975
|
|||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDiffsColorsToUserPreferences < Gitlab::Database::Migration[1.0]
|
||||
enable_lock_retries!
|
||||
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limit is added in 20220113164901_add_text_limit_to_user_preferences_diffs_colors.rb
|
||||
def change
|
||||
add_column :user_preferences, :diffs_deletion_color, :text
|
||||
add_column :user_preferences, :diffs_addition_color, :text
|
||||
end
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTextLimitToUserPreferencesDiffsColors < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :user_preferences, :diffs_deletion_color, 7
|
||||
add_text_limit :user_preferences, :diffs_addition_color, 7
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :user_preferences, :diffs_addition_color
|
||||
remove_text_limit :user_preferences, :diffs_deletion_color
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTmpIndexRoutesIdForProjectNamespaces < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'tmp_index_for_project_namespace_id_migration_on_routes'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
# Temporary index to be removed in 15.0
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/352353
|
||||
add_concurrent_index :routes, :id, where: "namespace_id IS NULL AND source_type = 'Project'", name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :routes, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillNamespaceIdForProjectRoutes < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
MIGRATION = 'BackfillNamespaceIdForProjectRoute'
|
||||
INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 1_000
|
||||
MAX_BATCH_SIZE = 10_000
|
||||
SUB_BATCH_SIZE = 200
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:routes,
|
||||
:id,
|
||||
job_interval: INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
max_batch_size: MAX_BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
Gitlab::Database::BackgroundMigration::BatchedMigration
|
||||
.for_configuration(MIGRATION, :routes, :id, [])
|
||||
.delete_all
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220113164801
Normal file
1
db/schema_migrations/20220113164801
Normal file
|
@ -0,0 +1 @@
|
|||
71526ea198c64d23a35f06804f30068591e937df22d74c262fdec9ecf04bf7d4
|
1
db/schema_migrations/20220113164901
Normal file
1
db/schema_migrations/20220113164901
Normal file
|
@ -0,0 +1 @@
|
|||
b157cec5eab77665ae57f02647c39dc0fb167d78e1894b395c46f59d791ab3e0
|
1
db/schema_migrations/20220322023800
Normal file
1
db/schema_migrations/20220322023800
Normal file
|
@ -0,0 +1 @@
|
|||
1aefb5950063a060de1ec20b0808a5488b238b36d86120c34ac5a128c212984e
|
1
db/schema_migrations/20220323023800
Normal file
1
db/schema_migrations/20220323023800
Normal file
|
@ -0,0 +1 @@
|
|||
1681c19d1f41a05c3dfeded70d128989afb4a81a2e04aacc8879c2c1ab766733
|
|
@ -21335,7 +21335,11 @@ CREATE TABLE user_preferences (
|
|||
experience_level smallint,
|
||||
view_diffs_file_by_file boolean DEFAULT false NOT NULL,
|
||||
gitpod_enabled boolean DEFAULT false NOT NULL,
|
||||
markdown_surround_selection boolean DEFAULT true NOT NULL
|
||||
markdown_surround_selection boolean DEFAULT true NOT NULL,
|
||||
diffs_deletion_color text,
|
||||
diffs_addition_color text,
|
||||
CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)),
|
||||
CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE user_preferences_id_seq
|
||||
|
@ -29663,6 +29667,8 @@ CREATE INDEX tmp_index_for_namespace_id_migration_on_routes ON routes USING btre
|
|||
|
||||
CREATE INDEX tmp_index_for_null_project_namespace_id ON projects USING btree (id) WHERE (project_namespace_id IS NULL);
|
||||
|
||||
CREATE INDEX tmp_index_for_project_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Project'::text));
|
||||
|
||||
CREATE INDEX tmp_index_issues_on_issue_type_and_id ON issues USING btree (issue_type, id);
|
||||
|
||||
CREATE INDEX tmp_index_members_on_state ON members USING btree (state) WHERE (state = 2);
|
||||
|
|
|
@ -24,7 +24,7 @@ GET /projects/:id/dora/metrics
|
|||
| Attribute | Type | Required | Description |
|
||||
|-------------- |-------- |----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../index.md#namespaced-path-encoding) can be accessed by the authenticated user. |
|
||||
| `metric` | string | yes | The [metric name](../../user/analytics/ci_cd_analytics.md#supported-metrics-in-gitlab). One of `deployment_frequency`, `lead_time_for_changes` or `time_to_restore_service`.|
|
||||
| `metric` | string | yes | The metric name: `deployment_frequency`, `lead_time_for_changes` or `time_to_restore_service`.|
|
||||
| `start_date` | string | no | Date range to start from. ISO 8601 Date format, for example `2021-03-01`. Default is 3 months ago. |
|
||||
| `end_date` | string | no | Date range to end at. ISO 8601 Date format, for example `2021-03-01`. Default is the current date. |
|
||||
| `interval` | string | no | The bucketing interval. One of `all`, `monthly` or `daily`. Default is `daily`. |
|
||||
|
@ -64,7 +64,7 @@ GET /groups/:id/dora/metrics
|
|||
| Attribute | Type | Required | Description |
|
||||
|-------------- |-------- |----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../index.md#namespaced-path-encoding) can be accessed by the authenticated user. |
|
||||
| `metric` | string | yes | The [metric name](../../user/analytics/ci_cd_analytics.md#supported-metrics-in-gitlab). One of `deployment_frequency`, `lead_time_for_changes`, `time_to_restore_service` or `change_failure_rate`. |
|
||||
| `metric` | string | yes | One of `deployment_frequency`, `lead_time_for_changes`, `time_to_restore_service` or `change_failure_rate`. |
|
||||
| `start_date` | string | no | Date range to start from. ISO 8601 Date format, for example `2021-03-01`. Default is 3 months ago. |
|
||||
| `end_date` | string | no | Date range to end at. ISO 8601 Date format, for example `2021-03-01`. Default is the current date. |
|
||||
| `interval` | string | no | The bucketing interval. One of `all`, `monthly` or `daily`. Default is `daily`. |
|
||||
|
|
|
@ -142,7 +142,7 @@ To enable the Dangerfile on another existing GitLab project, complete the follow
|
|||
1. Create a `Dangerfile` with the following content:
|
||||
|
||||
```ruby
|
||||
require_relative "lib/gitlab-dangerfiles"
|
||||
require "gitlab-dangerfiles"
|
||||
|
||||
Gitlab::Dangerfiles.for_project(self, &:import_defaults)
|
||||
```
|
||||
|
|
|
@ -34,34 +34,6 @@ To view CI/CD analytics:
|
|||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Analytics > CI/CD Analytics**.
|
||||
|
||||
## DevOps Research and Assessment (DORA) key metrics **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.7.
|
||||
> - [Added support](https://gitlab.com/gitlab-org/gitlab/-/issues/291746) for lead time for changes in GitLab 13.10.
|
||||
|
||||
The DevOps Research and Assessment ([DORA](https://cloud.google.com/blog/products/devops-sre/the-2019-accelerate-state-of-devops-elite-performance-productivity-and-scaling))
|
||||
team developed several key metrics that you can use as performance indicators for software development
|
||||
teams:
|
||||
|
||||
- Deployment frequency: How often an organization successfully releases to production.
|
||||
- Lead time for changes: The amount of time it takes for code to reach production.
|
||||
- Change failure rate: The percentage of deployments that cause a failure in production.
|
||||
- Time to restore service: How long it takes for an organization to recover from a failure in
|
||||
production.
|
||||
|
||||
### Supported metrics in GitLab
|
||||
|
||||
The following table shows the supported metrics, at which level they are supported, and which GitLab version (API and UI) they were introduced:
|
||||
|
||||
| Metric | Level | API version | Chart (UI) version | Comments |
|
||||
|---------------------------|---------------------|--------------------------------------|---------------------------------------|-----------|
|
||||
| `deployment_frequency` | Project-level | [13.7+](../../api/dora/metrics.md) | [13.8+](#view-deployment-frequency-chart) | The [old API endpoint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. |
|
||||
| `deployment_frequency` | Group-level | [13.10+](../../api/dora/metrics.md) | [13.12+](#view-deployment-frequency-chart) | |
|
||||
| `lead_time_for_changes` | Project-level | [13.10+](../../api/dora/metrics.md) | [13.11+](#view-lead-time-for-changes-chart) | Unit in seconds. Aggregation method is median. |
|
||||
| `lead_time_for_changes` | Group-level | [13.10+](../../api/dora/metrics.md) | [14.0+](#view-lead-time-for-changes-chart) | Unit in seconds. Aggregation method is median. |
|
||||
| `change_failure_rate` | Project/Group-level | To be supported | To be supported | |
|
||||
| `time_to_restore_service` | Project/Group-level | To be supported | To be supported | |
|
||||
|
||||
## View deployment frequency chart **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.8.
|
||||
|
|
|
@ -54,52 +54,82 @@ The following analytics features are available for users to create personalized
|
|||
|
||||
Be sure to review the documentation page for this feature for GitLab tier requirements.
|
||||
|
||||
## DevOps Research and Assessment (DORA) key metrics **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.7.
|
||||
> - [Added support](https://gitlab.com/gitlab-org/gitlab/-/issues/291746) for lead time for changes in GitLab 13.10.
|
||||
|
||||
The [DevOps Research and Assessment (DORA)](https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance)
|
||||
team developed several key metrics that you can use as performance indicators for software development
|
||||
teams.
|
||||
|
||||
### Deployment frequency
|
||||
|
||||
Deployment frequency is the frequency of successful deployments to production (hourly, daily, weekly, monthly, or yearly).
|
||||
This measures how often you deliver value to end users. A higher deployment frequency means you can
|
||||
get feedback sooner and iterate faster to deliver improvements and features. GitLab measures this as the number of
|
||||
deployments to a production environment in the given time period.
|
||||
|
||||
Deployment frequency displays in several charts:
|
||||
|
||||
- [Group-level value stream analytics](../group/value_stream_analytics/index.md)
|
||||
- [Project-level value stream analytics](value_stream_analytics.md)
|
||||
- [CI/CD analytics](ci_cd_analytics.md)
|
||||
|
||||
### Lead time for changes
|
||||
|
||||
Lead time for changes measures the time to deliver a feature once it has been developed,
|
||||
as described in [Measuring DevOps Performance](https://devops.com/measuring-devops-performance/).
|
||||
|
||||
Lead time for changes displays in several charts:
|
||||
|
||||
- [Group-level value stream analytics](../group/value_stream_analytics/index.md)
|
||||
- [Project-level value stream analytics](value_stream_analytics.md)
|
||||
- [CI/CD analytics](ci_cd_analytics.md)
|
||||
|
||||
### Time to restore service
|
||||
|
||||
Time to restore service measures how long it takes an organization to recover from a failure in production.
|
||||
GitLab measures this as the average time required to close the incidents
|
||||
in the given time period. This assumes:
|
||||
|
||||
- All incidents are related to a production environment.
|
||||
- Incidents and deployments have a strictly one-to-one relationship. An incident is related to only
|
||||
one production deployment, and any production deployment is related to no more than one incident).
|
||||
|
||||
To retrieve metrics for time to restore service, use the [GraphQL](../../api/graphql/reference/index.md) or the [REST](../../api/dora/metrics.md) APIs.
|
||||
|
||||
### Change failure rate
|
||||
|
||||
Change failure rate measures the percentage of deployments that cause a failure in production. GitLab measures this as the number
|
||||
of incidents divided by the number of deployments to a
|
||||
production environment in the given time period. This assumes:
|
||||
|
||||
- All incidents are related to a production environment.
|
||||
- Incidents and deployments have a strictly one-to-one relationship. An incident is related to only
|
||||
one production deployment, and any production deployment is related to no
|
||||
more than one incident.
|
||||
|
||||
NOTE:
|
||||
GitLab does not support the change failure rate metric.
|
||||
|
||||
### Supported DORA metrics in GitLab
|
||||
|
||||
| Metric | Level | API | UI chart | Comments |
|
||||
|---------------------------|-------------------------|-------------------------------------|---------------------------------------|-------------------------------|
|
||||
| `deployment_frequency` | Project | [GitLab 13.7 and later](../../api/dora/metrics.md) | GitLab 14.8 and later | The [previous API endpoint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. |
|
||||
| `deployment_frequency` | Group | [GitLab 13.10 and later](../../api/dora/metrics.md) | GitLab 13.12 and later | |
|
||||
| `lead_time_for_changes` | Project | [GitLab 13.10 and later](../../api/dora/metrics.md) | GitLab 13.11 and later | Unit in seconds. Aggregation method is median. |
|
||||
| `lead_time_for_changes` | Group | [GitLab 13.10 and later](../../api/dora/metrics.md) | GitLab 14.0 and later | Unit in seconds. Aggregation method is median. |
|
||||
| `time_to_restore_service` | Project and group | [GitLab 14.9 and later](../../api/dora/metrics.md) | Not supported | |
|
||||
| `change_failure_rate` | Project and group | Not supported | Not supported | |
|
||||
|
||||
## Definitions
|
||||
|
||||
We use the following terms to describe GitLab analytics:
|
||||
|
||||
- **Cycle time:** The duration of only the execution work. Cycle time is often displayed in combination with the lead time, which is longer than the cycle time. GitLab measures cycle time from the earliest commit of a [linked issue's merge request](../project/issues/crosslinking_issues.md) to when that issue is closed. The cycle time approach underestimates the lead time because merge request creation is always later than commit time. GitLab displays cycle time in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md) and [project-level Value Stream Analytics](../analytics/value_stream_analytics.md).
|
||||
- **Deploys:** The total number of successful deployments to production in the given time frame (across all applicable projects). GitLab displays deploys in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md) and [project-level Value Stream Analytics](value_stream_analytics.md).
|
||||
- **DORA (DevOps Research and Assessment)** ["Four Keys"](https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance):
|
||||
- **Speed/Velocity**
|
||||
|
||||
- **Deployment frequency:** The relative frequency of successful deployments to production
|
||||
(hourly, daily, weekly, monthly, or yearly).
|
||||
This measures how often you are delivering value to end users. A higher deployment
|
||||
frequency means you are able to get feedback and iterate faster to deliver
|
||||
improvements and features. GitLab measures this as the number of deployments to a
|
||||
[production environment](../../ci/environments/index.md#deployment-tier-of-environments) in
|
||||
the given time period.
|
||||
GitLab displays deployment frequency in [group-level Value Stream Analytics](../group/value_stream_analytics/index.md) and [project-level Value Stream Analytics](value_stream_analytics.md).
|
||||
- **Lead Time for Changes:** The time it takes for a commit to get into production. GitLab
|
||||
measures this as the median duration between merge request merge and deployment to a
|
||||
[production environment](../../ci/environments/index.md#deployment-tier-of-environments) for
|
||||
all MRs deployed in the given time period. This measure under estimates lead time because
|
||||
merge time is always later than commit time. The
|
||||
[standard definition](https://github.com/GoogleCloudPlatform/fourkeys/blob/main/METRICS.md#lead-time-for-changes) uses median commit time.
|
||||
[An issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/328459) to start
|
||||
measuring from "issue first commit" as a better proxy, although still imperfect.
|
||||
|
||||
- **Stability**
|
||||
- **Change Failure Rate:** The percentage of deployments causing a failure in production.
|
||||
GitLab measures this as the number of [incidents](../../operations/incident_management/incidents.md)
|
||||
divided by the number of deployments to a
|
||||
[production environment](../../ci/environments/index.md#deployment-tier-of-environments) in
|
||||
the given time period. This assumes:
|
||||
|
||||
- All incidents are related to a production environment.
|
||||
- Incidents and deployments have a strictly one-to-one relationship (meaning any incident is
|
||||
related to only one production deployment, and any production deployment is related to no
|
||||
more than one incident).
|
||||
|
||||
- **Time to Restore Service:** How long it takes an organization to recover from a failure in
|
||||
production. GitLab measures this as the average time required to close the
|
||||
[incidents](../../operations/incident_management/incidents.md) in the given time period.
|
||||
This assumes:
|
||||
|
||||
- All incidents are related to a [production environment](../../ci/environments/index.md#deployment-tier-of-environments).
|
||||
- Incidents and deployments have a strictly one-to-one relationship (meaning any incident is related to only one production deployment, and any production deployment is related to no more than one incident).
|
||||
|
||||
- **Lead time:** The duration of your value stream, from start to finish. Different to
|
||||
[Lead time for changes](#lead-time-for-changes). Often displayed in combination with "cycle time,"
|
||||
which is shorter. GitLab measures lead time from issue creation to issue close. GitLab displays lead
|
||||
|
@ -121,8 +151,3 @@ with "plan" and ends with "monitor". GitLab helps you track your value stream us
|
|||
- **Velocity:** The total issue burden completed in some period of time. The burden is usually measured
|
||||
in points or weight, often per sprint. For example, your velocity may be "30 points per sprint". GitLab
|
||||
measures velocity as the total points or weight of issues closed in a given period of time.
|
||||
|
||||
## Lead time for changes
|
||||
|
||||
"Lead Time for Changes" differs from "Lead Time" because it "focuses on measuring only the time to
|
||||
deliver a feature once it has been developed", as described in ([Measuring DevOps Performance](https://devops.com/measuring-devops-performance/)).
|
||||
|
|
|
@ -89,6 +89,20 @@ The default syntax theme is White, and you can choose among 5 different themes:
|
|||
|
||||
Introduced in GitLab 13.6, the themes [Solarized](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) and [Monokai](https://gitlab.com/gitlab-org/gitlab/-/issues/221034) also apply to the [Web IDE](../project/web_ide/index.md) and [Snippets](../snippets.md).
|
||||
|
||||
## Diff colors
|
||||
|
||||
A diff compares the old/removed content with the new/added content (e.g. when
|
||||
[reviewing a merge request](../project/merge_requests/reviews/index.md#review-a-merge-request) or in a
|
||||
[Markdown inline diff](../markdown.md#inline-diff)).
|
||||
Typically, the colors red and green are used for removed and added lines in diffs.
|
||||
The exact colors depend on the selected [syntax highlighting theme](#syntax-highlighting-theme).
|
||||
The colors may lead to difficulties in case of red–green color blindness.
|
||||
|
||||
For this reason, you can customize the following colors:
|
||||
|
||||
- Color for removed lines
|
||||
- Color for added lines
|
||||
|
||||
## Behavior
|
||||
|
||||
The following settings allow you to customize the behavior of the GitLab layout
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Backfills the `routes.namespace_id` column, by setting it to project.project_namespace_id
|
||||
class BackfillNamespaceIdForProjectRoute
|
||||
include Gitlab::Database::DynamicModelHelpers
|
||||
|
||||
def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, pause_ms)
|
||||
parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id)
|
||||
|
||||
parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch|
|
||||
cleanup_gin_index('routes')
|
||||
|
||||
batch_metrics.time_operation(:update_all) do
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
WITH route_and_ns(route_id, project_namespace_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
|
||||
#{sub_batch.to_sql}
|
||||
)
|
||||
UPDATE routes
|
||||
SET namespace_id = route_and_ns.project_namespace_id
|
||||
FROM route_and_ns
|
||||
WHERE id = route_and_ns.route_id
|
||||
SQL
|
||||
end
|
||||
|
||||
pause_ms = [0, pause_ms].max
|
||||
sleep(pause_ms * 0.001)
|
||||
end
|
||||
end
|
||||
|
||||
def batch_metrics
|
||||
@batch_metrics ||= Gitlab::Database::BackgroundMigration::BatchMetrics.new
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_gin_index(table_name)
|
||||
sql = "select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'"
|
||||
index_names = ActiveRecord::Base.connection.select_values(sql)
|
||||
|
||||
index_names.each do |index_name|
|
||||
ActiveRecord::Base.connection.execute("select gin_clean_pending_list('#{index_name}')")
|
||||
end
|
||||
end
|
||||
|
||||
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
|
||||
define_batchable_model(source_table, connection: ActiveRecord::Base.connection)
|
||||
.joins('INNER JOIN projects ON routes.source_id = projects.id')
|
||||
.where(source_key_column => start_id..stop_id)
|
||||
.where(namespace_id: nil)
|
||||
.where(source_type: 'Project')
|
||||
.where.not(projects: { project_namespace_id: nil })
|
||||
.select("routes.id, projects.project_namespace_id")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -28363,6 +28363,12 @@ msgstr ""
|
|||
msgid "Preferences|Choose what content you want to see on your homepage."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Color for added lines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Color for removed lines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Configure how dates and times display for you."
|
||||
msgstr ""
|
||||
|
||||
|
@ -28372,6 +28378,12 @@ msgstr ""
|
|||
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Customize the colors of removed and added lines in diffs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Diff colors"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Display time in 24-hour format"
|
||||
msgstr ""
|
||||
|
||||
|
@ -28408,6 +28420,9 @@ msgstr ""
|
|||
msgid "Preferences|Navigation theme"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Project overview content"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33404,6 +33419,9 @@ msgstr ""
|
|||
msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration| and "
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration| or "
|
||||
msgstr ""
|
||||
|
||||
|
@ -33632,6 +33650,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Summary"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|The following branches do not exist on this development project: %{branches}. Please review all branches to ensure the values are accurate before updating this policy."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|There was a problem creating the new security policy"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36507,6 +36528,12 @@ msgstr ""
|
|||
msgid "SuggestedColors|Crimson"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Current addition color"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Current removal color"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Dark coral"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36522,6 +36549,12 @@ msgstr ""
|
|||
msgid "SuggestedColors|Deep violet"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Default addition color"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Default removal color"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Gray"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36540,6 +36573,9 @@ msgstr ""
|
|||
msgid "SuggestedColors|Medium sea green"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Orange"
|
||||
msgstr ""
|
||||
|
||||
msgid "SuggestedColors|Red"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ RSpec.describe Profiles::PreferencesController do
|
|||
it "changes the user's preferences" do
|
||||
prefs = {
|
||||
color_scheme_id: '1',
|
||||
diffs_deletion_color: '#123456',
|
||||
diffs_addition_color: '#abcdef',
|
||||
dashboard: 'stars',
|
||||
theme_id: '2',
|
||||
first_day_of_week: '1',
|
||||
|
@ -84,5 +86,27 @@ RSpec.describe Profiles::PreferencesController do
|
|||
expect(response.parsed_body['type']).to eq('alert')
|
||||
end
|
||||
end
|
||||
|
||||
context 'on invalid diffs colors setting' do
|
||||
it 'responds with error for diffs_deletion_color' do
|
||||
prefs = { diffs_deletion_color: '#1234567' }
|
||||
|
||||
go params: prefs
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response.parsed_body['message']).to eq _('Failed to save preferences.')
|
||||
expect(response.parsed_body['type']).to eq('alert')
|
||||
end
|
||||
|
||||
it 'responds with error for diffs_addition_color' do
|
||||
prefs = { diffs_addition_color: '#1234567' }
|
||||
|
||||
go params: prefs
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response.parsed_body['message']).to eq _('Failed to save preferences.')
|
||||
expect(response.parsed_body['type']).to eq('alert')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,14 +18,6 @@ RSpec.describe 'User visits the profile preferences page', :js do
|
|||
end
|
||||
|
||||
describe 'User changes their syntax highlighting theme', :js do
|
||||
it 'creates a flash message' do
|
||||
choose 'user_color_scheme_id_5'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect_preferences_saved_message
|
||||
end
|
||||
|
||||
it 'updates their preference' do
|
||||
choose 'user_color_scheme_id_5'
|
||||
|
||||
|
|
|
@ -1733,4 +1733,36 @@ describe('Api', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectProtectedBranch', () => {
|
||||
const branchName = 'new-branch-name';
|
||||
const dummyProjectId = 5;
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${dummyProjectId}/protected_branches/${branchName}`;
|
||||
|
||||
it('returns 404 for non-existing branch', () => {
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(httpStatus.NOT_FOUND, {
|
||||
message: '404 Not found',
|
||||
});
|
||||
|
||||
return Api.projectProtectedBranch(dummyProjectId, branchName).catch((error) => {
|
||||
expect(error.response.status).toBe(httpStatus.NOT_FOUND);
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns 200 with branch information', () => {
|
||||
const expectedObj = { name: branchName };
|
||||
|
||||
jest.spyOn(axios, 'get');
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, expectedObj);
|
||||
|
||||
return Api.projectProtectedBranch(dummyProjectId, branchName).then((data) => {
|
||||
expect(data).toEqual(expectedObj);
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,915 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
|
||||
<div
|
||||
class="form-group"
|
||||
>
|
||||
<label>
|
||||
Preview
|
||||
</label>
|
||||
|
||||
<table
|
||||
class="code"
|
||||
>
|
||||
<tbody>
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="1"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="c1"
|
||||
>
|
||||
#
|
||||
<span
|
||||
class="idiff deletion"
|
||||
>
|
||||
Removed
|
||||
</span>
|
||||
content
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="1"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="c1"
|
||||
>
|
||||
#
|
||||
<span
|
||||
class="idiff addition"
|
||||
>
|
||||
Added
|
||||
</span>
|
||||
content
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="2"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
v
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="2"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
v
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="3"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
s
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="s"
|
||||
>
|
||||
"string"
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="3"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
s
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="s"
|
||||
>
|
||||
"string"
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="4"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span />
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="4"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="5"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
for
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="ow"
|
||||
>
|
||||
in
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
range
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
10
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
10
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="5"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
for
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="ow"
|
||||
>
|
||||
in
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
range
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
10
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
10
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="6"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
print
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="6"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
print
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
i
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
+
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mi"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="7"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span />
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="7"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="8"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
class
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nc"
|
||||
>
|
||||
LinkedList
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
object
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="8"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
class
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nc"
|
||||
>
|
||||
LinkedList
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
object
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="9"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
def
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nf"
|
||||
>
|
||||
__init__
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
x
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="9"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="k"
|
||||
>
|
||||
def
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="nf"
|
||||
>
|
||||
__init__
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
(
|
||||
</span>
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
x
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
):
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="10"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
val
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="10"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
val
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="n"
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
class="line_holder parallel"
|
||||
>
|
||||
<td
|
||||
class="old_line diff-line-num old"
|
||||
>
|
||||
<a
|
||||
data-linenumber="11"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel left-side old"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
next
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
None
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="new_line diff-line-num new"
|
||||
>
|
||||
<a
|
||||
data-linenumber="11"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="line_content parallel right-side new"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
self
|
||||
</span>
|
||||
<span
|
||||
class="p"
|
||||
>
|
||||
.
|
||||
</span>
|
||||
<span
|
||||
class="nb"
|
||||
>
|
||||
next
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="o"
|
||||
>
|
||||
=
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="bp"
|
||||
>
|
||||
None
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,23 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue';
|
||||
|
||||
describe('DiffsColorsPreview component', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent() {
|
||||
wrapper = shallowMount(DiffsColorsPreview);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('renders diff colors preview', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,153 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { s__ } from '~/locale';
|
||||
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
|
||||
import DiffsColors from '~/profile/preferences/components/diffs_colors.vue';
|
||||
import DiffsColorsPreview from '~/profile/preferences/components/diffs_colors_preview.vue';
|
||||
import * as CssUtils from '~/lib/utils/css_utils';
|
||||
|
||||
describe('DiffsColors component', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultInjectedProps = {
|
||||
addition: '#00ff00',
|
||||
deletion: '#ff0000',
|
||||
};
|
||||
|
||||
const initialSuggestedColors = {
|
||||
'#d99530': s__('SuggestedColors|Orange'),
|
||||
'#1f75cb': s__('SuggestedColors|Blue'),
|
||||
};
|
||||
|
||||
const findColorPickers = () => wrapper.findAllComponents(ColorPicker);
|
||||
|
||||
function createComponent(provide = {}) {
|
||||
wrapper = shallowMount(DiffsColors, {
|
||||
provide: {
|
||||
...defaultInjectedProps,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('mounts', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('preview', () => {
|
||||
it('should render preview', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(DiffsColorsPreview).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set preview classes', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.attributes('class')).toBe(
|
||||
'diff-custom-addition-color diff-custom-deletion-color',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ addition: null }, 'diff-custom-deletion-color'],
|
||||
[{ deletion: null }, 'diff-custom-addition-color'],
|
||||
])('should not set preview class if color not set', (provide, expectedClass) => {
|
||||
createComponent(provide);
|
||||
|
||||
expect(wrapper.attributes('class')).toBe(expectedClass);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, '--diff-deletion-color: rgba(255,0,0,0.2); --diff-addition-color: rgba(0,255,0,0.2);'],
|
||||
[{ addition: null }, '--diff-deletion-color: rgba(255,0,0,0.2);'],
|
||||
[{ deletion: null }, '--diff-addition-color: rgba(0,255,0,0.2);'],
|
||||
])('should set correct CSS variables', (provide, expectedStyle) => {
|
||||
createComponent(provide);
|
||||
|
||||
expect(wrapper.attributes('style')).toBe(expectedStyle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('color pickers', () => {
|
||||
it('should render both color pickers', () => {
|
||||
createComponent();
|
||||
|
||||
const colorPickers = findColorPickers();
|
||||
|
||||
expect(colorPickers.length).toBe(2);
|
||||
expect(colorPickers.at(0).props()).toMatchObject({
|
||||
label: s__('Preferences|Color for removed lines'),
|
||||
value: '#ff0000',
|
||||
state: true,
|
||||
});
|
||||
expect(colorPickers.at(1).props()).toMatchObject({
|
||||
label: s__('Preferences|Color for added lines'),
|
||||
value: '#00ff00',
|
||||
state: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('suggested colors', () => {
|
||||
const suggestedColors = () => findColorPickers().at(0).props('suggestedColors');
|
||||
|
||||
it('contains initial suggested colors', () => {
|
||||
createComponent();
|
||||
|
||||
expect(suggestedColors()).toMatchObject(initialSuggestedColors);
|
||||
});
|
||||
|
||||
it('contains default diff colors of theme', () => {
|
||||
jest.spyOn(CssUtils, 'getCssVariable').mockImplementation((variable) => {
|
||||
if (variable === '--default-diff-color-addition') return '#111111';
|
||||
if (variable === '--default-diff-color-deletion') return '#222222';
|
||||
return '#000000';
|
||||
});
|
||||
|
||||
createComponent();
|
||||
|
||||
expect(suggestedColors()).toMatchObject({
|
||||
'#111111': s__('SuggestedColors|Default addition color'),
|
||||
'#222222': s__('SuggestedColors|Default removal color'),
|
||||
});
|
||||
});
|
||||
|
||||
it('contains current diff colors if set', () => {
|
||||
createComponent();
|
||||
|
||||
expect(suggestedColors()).toMatchObject({
|
||||
[defaultInjectedProps.addition]: s__('SuggestedColors|Current addition color'),
|
||||
[defaultInjectedProps.deletion]: s__('SuggestedColors|Current removal color'),
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
{ addition: null },
|
||||
s__('SuggestedColors|Current removal color'),
|
||||
s__('SuggestedColors|Current addition color'),
|
||||
],
|
||||
[
|
||||
{ deletion: null },
|
||||
s__('SuggestedColors|Current addition color'),
|
||||
s__('SuggestedColors|Current removal color'),
|
||||
],
|
||||
])(
|
||||
'does not contain current diff color if not set %p',
|
||||
(provide, expectedToContain, expectNotToContain) => {
|
||||
createComponent(provide);
|
||||
|
||||
const suggestedColorsLabels = Object.values(suggestedColors());
|
||||
expect(suggestedColorsLabels).toContain(expectedToContain);
|
||||
expect(suggestedColorsLabels).not.toContain(expectNotToContain);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
89
spec/helpers/colors_helper_spec.rb
Normal file
89
spec/helpers/colors_helper_spec.rb
Normal file
|
@ -0,0 +1,89 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ColorsHelper do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
describe '#hex_color_to_rgb_array' do
|
||||
context 'valid hex color' do
|
||||
where(:hex_color, :rgb_array) do
|
||||
'#000000' | [0, 0, 0]
|
||||
'#aaaaaa' | [170, 170, 170]
|
||||
'#cCcCcC' | [204, 204, 204]
|
||||
'#FFFFFF' | [255, 255, 255]
|
||||
'#000abc' | [0, 10, 188]
|
||||
'#123456' | [18, 52, 86]
|
||||
'#a1b2c3' | [161, 178, 195]
|
||||
'#000' | [0, 0, 0]
|
||||
'#abc' | [170, 187, 204]
|
||||
'#321' | [51, 34, 17]
|
||||
'#7E2' | [119, 238, 34]
|
||||
'#fFf' | [255, 255, 255]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns correct RGB array' do
|
||||
expect(helper.hex_color_to_rgb_array(hex_color)).to eq(rgb_array)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid hex color' do
|
||||
where(:hex_color) { ['', '0', '#00', '#ffff', '#1234567', 'invalid', [], 1, nil] }
|
||||
|
||||
with_them do
|
||||
it 'raise ArgumentError' do
|
||||
expect { helper.hex_color_to_rgb_array(hex_color) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rgb_array_to_hex_color' do
|
||||
context 'valid RGB array' do
|
||||
where(:rgb_array, :hex_color) do
|
||||
[0, 0, 0] | '#000000'
|
||||
[0, 0, 255] | '#0000ff'
|
||||
[0, 255, 0] | '#00ff00'
|
||||
[255, 0, 0] | '#ff0000'
|
||||
[12, 34, 56] | '#0c2238'
|
||||
[222, 111, 88] | '#de6f58'
|
||||
[255, 255, 255] | '#ffffff'
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'returns correct hex color' do
|
||||
expect(helper.rgb_array_to_hex_color(rgb_array)).to eq(hex_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'invalid RGB array' do
|
||||
where(:rgb_array) do
|
||||
[
|
||||
'',
|
||||
'#000000',
|
||||
0,
|
||||
nil,
|
||||
[],
|
||||
[0],
|
||||
[0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[-1, 0, 0],
|
||||
[0, -1, 0],
|
||||
[0, 0, -1],
|
||||
[256, 0, 0],
|
||||
[0, 256, 0],
|
||||
[0, 0, 256]
|
||||
]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'raise ArgumentError' do
|
||||
expect { helper.rgb_array_to_hex_color(rgb_array) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -145,6 +145,67 @@ RSpec.describe PreferencesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#user_diffs_colors' do
|
||||
context 'with a user' do
|
||||
it "returns user's diffs colors" do
|
||||
stub_user(diffs_addition_color: '#123456', diffs_deletion_color: '#abcdef')
|
||||
|
||||
expect(helper.user_diffs_colors).to eq({ addition: '#123456', deletion: '#abcdef' })
|
||||
end
|
||||
|
||||
it 'omits property if nil' do
|
||||
stub_user(diffs_addition_color: '#123456', diffs_deletion_color: nil)
|
||||
|
||||
expect(helper.user_diffs_colors).to eq({ addition: '#123456' })
|
||||
end
|
||||
|
||||
it 'omits property if blank' do
|
||||
stub_user(diffs_addition_color: '', diffs_deletion_color: '#abcdef')
|
||||
|
||||
expect(helper.user_diffs_colors).to eq({ deletion: '#abcdef' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user' do
|
||||
it 'returns no properties' do
|
||||
stub_user
|
||||
|
||||
expect(helper.user_diffs_colors).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#custom_diff_color_classes' do
|
||||
context 'with a user' do
|
||||
it 'returns color classes' do
|
||||
stub_user(diffs_addition_color: '#123456', diffs_deletion_color: '#abcdef')
|
||||
|
||||
expect(helper.custom_diff_color_classes)
|
||||
.to match_array(%w[diff-custom-addition-color diff-custom-deletion-color])
|
||||
end
|
||||
|
||||
it 'omits property if nil' do
|
||||
stub_user(diffs_addition_color: '#123456', diffs_deletion_color: nil)
|
||||
|
||||
expect(helper.custom_diff_color_classes).to match_array(['diff-custom-addition-color'])
|
||||
end
|
||||
|
||||
it 'omits property if blank' do
|
||||
stub_user(diffs_addition_color: '', diffs_deletion_color: '#abcdef')
|
||||
|
||||
expect(helper.custom_diff_color_classes).to match_array(['diff-custom-deletion-color'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user' do
|
||||
it 'returns no classes' do
|
||||
stub_user
|
||||
|
||||
expect(helper.custom_diff_color_classes).to match_array([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#language_choices' do
|
||||
include StubLanguagesTranslationPercentage
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForProjectRoute do
|
||||
let(:migration) { described_class.new }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:routes) { table(:routes) }
|
||||
|
||||
let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') }
|
||||
let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') }
|
||||
let(:namespace3) { namespaces.create!(name: 'batchtest3', type: 'Group', parent_id: namespace2.id, path: 'space3') }
|
||||
|
||||
let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id) }
|
||||
let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id) }
|
||||
let(:proj_namespace3) { namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: namespace3.id) }
|
||||
let(:proj_namespace4) { namespaces.create!(name: 'proj4', path: 'proj4', type: 'Project', parent_id: namespace3.id) }
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) }
|
||||
let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) }
|
||||
let(:proj3) { projects.create!(name: 'proj3', path: 'proj3', namespace_id: namespace3.id, project_namespace_id: proj_namespace3.id) }
|
||||
let(:proj4) { projects.create!(name: 'proj4', path: 'proj4', namespace_id: namespace3.id, project_namespace_id: proj_namespace4.id) }
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
let!(:namespace_route1) { routes.create!(path: 'space1', source_id: namespace1.id, source_type: 'Namespace') }
|
||||
let!(:namespace_route2) { routes.create!(path: 'space1/space2', source_id: namespace2.id, source_type: 'Namespace') }
|
||||
let!(:namespace_route3) { routes.create!(path: 'space1/space3', source_id: namespace3.id, source_type: 'Namespace') }
|
||||
|
||||
let!(:proj_route1) { routes.create!(path: 'space1/proj1', source_id: proj1.id, source_type: 'Project') }
|
||||
let!(:proj_route2) { routes.create!(path: 'space1/space2/proj2', source_id: proj2.id, source_type: 'Project') }
|
||||
let!(:proj_route3) { routes.create!(path: 'space1/space3/proj3', source_id: proj3.id, source_type: 'Project') }
|
||||
let!(:proj_route4) { routes.create!(path: 'space1/space3/proj4', source_id: proj4.id, source_type: 'Project') }
|
||||
|
||||
subject(:perform_migration) { migration.perform(proj_route1.id, proj_route4.id, :routes, :id, 2, 0) }
|
||||
|
||||
it 'backfills namespace_id for the selected records', :aggregate_failures do
|
||||
perform_migration
|
||||
|
||||
expected_namespaces = [proj_namespace1.id, proj_namespace2.id, proj_namespace3.id, proj_namespace4.id]
|
||||
|
||||
expected_projects = [proj_route1.id, proj_route2.id, proj_route3.id, proj_route4.id]
|
||||
expect(routes.where.not(namespace_id: nil).pluck(:id)).to match_array(expected_projects)
|
||||
expect(routes.where.not(namespace_id: nil).pluck(:namespace_id)).to match_array(expected_namespaces)
|
||||
end
|
||||
|
||||
it 'tracks timings of queries' do
|
||||
expect(migration.batch_metrics.timings).to be_empty
|
||||
|
||||
expect { perform_migration }.to change { migration.batch_metrics.timings }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe BackfillNamespaceIdForProjectRoutes, :migration do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules background jobs for each batch of group members' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
table_name: :routes,
|
||||
column_name: :id,
|
||||
interval: described_class::INTERVAL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,48 @@ require 'spec_helper'
|
|||
RSpec.describe UserPreference do
|
||||
let(:user_preference) { create(:user_preference) }
|
||||
|
||||
describe 'validations' do
|
||||
describe 'diffs_deletion_color and diffs_addition_color' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(color: [
|
||||
'#000000',
|
||||
'#123456',
|
||||
'#abcdef',
|
||||
'#AbCdEf',
|
||||
'#ffffff',
|
||||
'#fFfFfF',
|
||||
'#000',
|
||||
'#123',
|
||||
'#abc',
|
||||
'#AbC',
|
||||
'#fff',
|
||||
'#fFf',
|
||||
''
|
||||
])
|
||||
|
||||
with_them do
|
||||
it { is_expected.to allow_value(color).for(:diffs_deletion_color) }
|
||||
it { is_expected.to allow_value(color).for(:diffs_addition_color) }
|
||||
end
|
||||
|
||||
where(color: [
|
||||
'#1',
|
||||
'#12',
|
||||
'#1234',
|
||||
'#12345',
|
||||
'#1234567',
|
||||
'123456',
|
||||
'#12345x'
|
||||
])
|
||||
|
||||
with_them do
|
||||
it { is_expected.not_to allow_value(color).for(:diffs_deletion_color) }
|
||||
it { is_expected.not_to allow_value(color).for(:diffs_addition_color) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'notes filters global keys' do
|
||||
it 'contains expected values' do
|
||||
expect(UserPreference::NOTES_FILTERS.keys).to match_array([:all_notes, :only_comments, :only_activity])
|
||||
|
|
|
@ -69,6 +69,12 @@ RSpec.describe User do
|
|||
it { is_expected.to delegate_method(:markdown_surround_selection).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:markdown_surround_selection=).to(:user_preference).with_arguments(:args) }
|
||||
|
||||
it { is_expected.to delegate_method(:diffs_deletion_color).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:diffs_deletion_color=).to(:user_preference).with_arguments(:args) }
|
||||
|
||||
it { is_expected.to delegate_method(:diffs_addition_color).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:diffs_addition_color=).to(:user_preference).with_arguments(:args) }
|
||||
|
||||
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
|
|
|
@ -8,28 +8,37 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ImageTypeRegex = regexp.MustCompile(`^image/*`)
|
||||
SvgMimeTypeRegex = regexp.MustCompile(`^image/svg\+xml$`)
|
||||
javaScriptTypeRegex = regexp.MustCompile(`^(text|application)\/javascript$`)
|
||||
|
||||
TextTypeRegex = regexp.MustCompile(`^text/*`)
|
||||
imageTypeRegex = regexp.MustCompile(`^image/*`)
|
||||
svgMimeTypeRegex = regexp.MustCompile(`^image/svg\+xml$`)
|
||||
|
||||
VideoTypeRegex = regexp.MustCompile(`^video/*`)
|
||||
textTypeRegex = regexp.MustCompile(`^text/*`)
|
||||
|
||||
PdfTypeRegex = regexp.MustCompile(`application\/pdf`)
|
||||
videoTypeRegex = regexp.MustCompile(`^video/*`)
|
||||
|
||||
AttachmentRegex = regexp.MustCompile(`^attachment`)
|
||||
InlineRegex = regexp.MustCompile(`^inline`)
|
||||
pdfTypeRegex = regexp.MustCompile(`application\/pdf`)
|
||||
|
||||
attachmentRegex = regexp.MustCompile(`^attachment`)
|
||||
inlineRegex = regexp.MustCompile(`^inline`)
|
||||
)
|
||||
|
||||
// Mime types that can't be inlined. Usually subtypes of main types
|
||||
var forbiddenInlineTypes = []*regexp.Regexp{SvgMimeTypeRegex}
|
||||
var forbiddenInlineTypes = []*regexp.Regexp{svgMimeTypeRegex}
|
||||
|
||||
// Mime types that can be inlined. We can add global types like "image/" or
|
||||
// specific types like "text/plain". If there is a specific type inside a global
|
||||
// allowed type that can't be inlined we must add it to the forbiddenInlineTypes var.
|
||||
// One example of this is the mime type "image". We allow all images to be
|
||||
// inlined except for SVGs.
|
||||
var allowedInlineTypes = []*regexp.Regexp{ImageTypeRegex, TextTypeRegex, VideoTypeRegex, PdfTypeRegex}
|
||||
var allowedInlineTypes = []*regexp.Regexp{imageTypeRegex, textTypeRegex, videoTypeRegex, pdfTypeRegex}
|
||||
|
||||
const (
|
||||
svgContentType = "image/svg+xml"
|
||||
textPlainContentType = "text/plain; charset=utf-8"
|
||||
attachmentDispositionText = "attachment"
|
||||
inlineDispositionText = "inline"
|
||||
)
|
||||
|
||||
func SafeContentHeaders(data []byte, contentDisposition string) (string, string) {
|
||||
contentType := safeContentType(data)
|
||||
|
@ -40,16 +49,24 @@ func SafeContentHeaders(data []byte, contentDisposition string) (string, string)
|
|||
func safeContentType(data []byte) string {
|
||||
// Special case for svg because DetectContentType detects it as text
|
||||
if svg.Is(data) {
|
||||
return "image/svg+xml"
|
||||
return svgContentType
|
||||
}
|
||||
|
||||
// Override any existing Content-Type header from other ResponseWriters
|
||||
contentType := http.DetectContentType(data)
|
||||
|
||||
// http.DetectContentType does not support JavaScript and would only
|
||||
// return text/plain. But for cautionary measures, just in case they start supporting
|
||||
// it down the road and start returning application/javascript, we want to handle it now
|
||||
// to avoid regressions.
|
||||
if isType(contentType, javaScriptTypeRegex) {
|
||||
return textPlainContentType
|
||||
}
|
||||
|
||||
// If the content is text type, we set to plain, because we don't
|
||||
// want to render it inline if they're html or javascript
|
||||
if isType(contentType, TextTypeRegex) {
|
||||
return "text/plain; charset=utf-8"
|
||||
if isType(contentType, textTypeRegex) {
|
||||
return textPlainContentType
|
||||
}
|
||||
|
||||
return contentType
|
||||
|
@ -58,7 +75,7 @@ func safeContentType(data []byte) string {
|
|||
func safeContentDisposition(contentType string, contentDisposition string) string {
|
||||
// If the existing disposition is attachment we return that. This allow us
|
||||
// to force a download from GitLab (ie: RawController)
|
||||
if AttachmentRegex.MatchString(contentDisposition) {
|
||||
if attachmentRegex.MatchString(contentDisposition) {
|
||||
return contentDisposition
|
||||
}
|
||||
|
||||
|
@ -82,11 +99,11 @@ func safeContentDisposition(contentType string, contentDisposition string) strin
|
|||
|
||||
func attachmentDisposition(contentDisposition string) string {
|
||||
if contentDisposition == "" {
|
||||
return "attachment"
|
||||
return attachmentDispositionText
|
||||
}
|
||||
|
||||
if InlineRegex.MatchString(contentDisposition) {
|
||||
return InlineRegex.ReplaceAllString(contentDisposition, "attachment")
|
||||
if inlineRegex.MatchString(contentDisposition) {
|
||||
return inlineRegex.ReplaceAllString(contentDisposition, attachmentDispositionText)
|
||||
}
|
||||
|
||||
return contentDisposition
|
||||
|
@ -94,11 +111,11 @@ func attachmentDisposition(contentDisposition string) string {
|
|||
|
||||
func inlineDisposition(contentDisposition string) string {
|
||||
if contentDisposition == "" {
|
||||
return "inline"
|
||||
return inlineDispositionText
|
||||
}
|
||||
|
||||
if AttachmentRegex.MatchString(contentDisposition) {
|
||||
return AttachmentRegex.ReplaceAllString(contentDisposition, "inline")
|
||||
if attachmentRegex.MatchString(contentDisposition) {
|
||||
return attachmentRegex.ReplaceAllString(contentDisposition, inlineDispositionText)
|
||||
}
|
||||
|
||||
return contentDisposition
|
||||
|
|
|
@ -56,11 +56,17 @@ func TestSetProperContentTypeAndDisposition(t *testing.T) {
|
|||
body: "<html><body>Hello world!</body></html>",
|
||||
},
|
||||
{
|
||||
desc: "Javascript type",
|
||||
desc: "Javascript within HTML type",
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
contentDisposition: "inline",
|
||||
body: "<script>alert(\"foo\")</script>",
|
||||
},
|
||||
{
|
||||
desc: "Javascript type",
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
contentDisposition: "inline",
|
||||
body: "alert(\"foo\")",
|
||||
},
|
||||
{
|
||||
desc: "Image type",
|
||||
contentType: "image/png",
|
||||
|
@ -170,25 +176,41 @@ func TestSetProperContentTypeAndDisposition(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFailOverrideContentType(t *testing.T) {
|
||||
testCase := struct {
|
||||
contentType string
|
||||
body string
|
||||
testCases := []struct {
|
||||
desc string
|
||||
overrideFromUpstream string
|
||||
responseContentType string
|
||||
body string
|
||||
}{
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
body: "<html><body>Hello world!</body></html>",
|
||||
{
|
||||
desc: "Force text/html into text/plain",
|
||||
responseContentType: "text/plain; charset=utf-8",
|
||||
overrideFromUpstream: "text/html; charset=utf-8",
|
||||
body: "<html><body>Hello world!</body></html>",
|
||||
},
|
||||
{
|
||||
desc: "Force application/javascript into text/plain",
|
||||
responseContentType: "text/plain; charset=utf-8",
|
||||
overrideFromUpstream: "application/javascript; charset=utf-8",
|
||||
body: "alert(1);",
|
||||
},
|
||||
}
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
// We are pretending to be upstream or an inner layer of the ResponseWriter chain
|
||||
w.Header().Set(headers.GitlabWorkhorseDetectContentTypeHeader, "true")
|
||||
w.Header().Set(headers.ContentTypeHeader, "text/html; charset=utf-8")
|
||||
_, err := io.WriteString(w, testCase.body)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
// We are pretending to be upstream or an inner layer of the ResponseWriter chain
|
||||
w.Header().Set(headers.GitlabWorkhorseDetectContentTypeHeader, "true")
|
||||
w.Header().Set(headers.ContentTypeHeader, tc.overrideFromUpstream)
|
||||
_, err := io.WriteString(w, tc.body)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
resp := makeRequest(t, h, testCase.body, "")
|
||||
resp := makeRequest(t, h, tc.body, "")
|
||||
|
||||
require.Equal(t, testCase.contentType, resp.Header.Get(headers.ContentTypeHeader))
|
||||
require.Equal(t, tc.responseContentType, resp.Header.Get(headers.ContentTypeHeader))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccessOverrideContentDispositionFromInlineToAttachment(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue