Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-02 18:09:03 +00:00
parent 215cb09934
commit 77cf68da37
77 changed files with 1571 additions and 862 deletions

View File

@ -7,7 +7,6 @@
before_script:
- '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb'
- cd qa/
- gem install bundler -v 1.17.3
- bundle install --clean --jobs=$(nproc) --path=vendor --retry=3 --without=development --quiet
- bundle check

View File

@ -183,7 +183,6 @@ update-coverage-cache:
- .shared:rules:update-cache
stage: prepare
script:
- run_timed_command "gem install bundler -v 1.17.3"
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
cache:
policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up.
@ -289,6 +288,7 @@ db:migrate-from-v12.10.0:
- git checkout -f FETCH_HEAD
- sed -i -e "s/gem 'grpc', '~> 1.24.0'/gem 'grpc', '~> 1.30.2'/" Gemfile # Update gRPC for Ruby 2.7
- sed -i -e "s/gem 'google-protobuf', '~> 3.8.0'/gem 'google-protobuf', '~> 3.12.0'/" Gemfile
- gem install bundler:1.17.3
- bundle update google-protobuf grpc bootsnap
- bundle install $BUNDLE_INSTALL_FLAGS
- date
@ -363,7 +363,6 @@ rspec:coverage:
- memory-static
- memory-on-boot
script:
- run_timed_command "gem install bundler -v 1.17.3"
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
- run_timed_command "bundle exec scripts/merge-simplecov"
- run_timed_command "bundle exec scripts/gather-test-memory-data"
@ -401,7 +400,6 @@ rspec:feature-flags:
- memory-static
- memory-on-boot
script:
- run_timed_command "gem install bundler -v 1.17.3"
- run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519"
- run_timed_command "bundle exec scripts/used-feature-flags"
# EE/FOSS: default refs (MRs, master, schedules) jobs #

View File

@ -39,7 +39,7 @@ update-tests-metadata:
- rspec-ee integration pg11 geo
- rspec-ee system pg11 geo
script:
- run_timed_command "retry gem install bundler:1.17.3 fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- run_timed_command "retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- source ./scripts/rspec_helpers.sh
- update_tests_metadata
- update_tests_mapping

View File

@ -119,7 +119,7 @@ gem 'fog-aws', '~> 3.5'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0'
gem 'fog-google', '~> 1.10'
gem 'fog-google', '~> 1.11'
gem 'fog-local', '~> 0.6'
gem 'fog-openstack', '~> 1.0'
gem 'fog-rackspace', '~> 0.1.1'

View File

@ -369,11 +369,12 @@ GEM
excon (~> 0.58)
formatador (~> 0.2)
mime-types
fog-google (1.10.0)
fog-google (1.11.0)
fog-core (<= 2.1.0)
fog-json (~> 1.2)
fog-xml (~> 0.1.0)
google-api-client (>= 0.32, < 0.34)
google-cloud-env (~> 1.2)
fog-json (1.2.0)
fog-core
multi_json (~> 1.10)
@ -475,6 +476,8 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-env (1.4.0)
faraday (>= 0.17.3, < 2.0)
google-protobuf (3.12.4)
googleapis-common-protos-types (1.0.5)
google-protobuf (~> 3.11)
@ -1325,7 +1328,7 @@ DEPENDENCIES
fog-aliyun (~> 0.3)
fog-aws (~> 3.5)
fog-core (= 2.1.0)
fog-google (~> 1.10)
fog-google (~> 1.11)
fog-local (~> 0.6)
fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1)
@ -1521,4 +1524,4 @@ DEPENDENCIES
yajl-ruby (~> 1.4.1)
BUNDLED WITH
1.17.3
2.1.4

View File

@ -1,103 +0,0 @@
<script>
import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
components: {
GlIcon,
},
directives: {
tooltip,
},
props: {
files: {
type: Array,
required: true,
},
iconName: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
computed: {
addedFilesLength() {
return this.files.filter(f => f.tempFile).length;
},
modifiedFilesLength() {
return this.files.filter(f => !f.tempFile).length;
},
addedFilesIconClass() {
return this.addedFilesLength ? 'multi-file-addition' : '';
},
modifiedFilesClass() {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(
n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength),
{
type: this.title.toLowerCase(),
count: this.addedFilesLength,
},
);
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength),
{
type: this.title.toLowerCase(),
count: this.modifiedFilesLength,
},
);
},
titleTooltip() {
return sprintf(__('%{title} changes'), { title: this.title });
},
additionIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
},
modifiedIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
},
},
};
</script>
<template>
<div class="multi-file-commit-list-collapsed text-center">
<div
v-tooltip
:title="titleTooltip"
data-container="body"
data-placement="left"
class="gl-mb-5"
>
<gl-icon v-once :name="iconName" :size="18" />
</div>
<div
v-tooltip
:title="additionsTooltip"
data-container="body"
data-placement="left"
class="gl-mb-3"
>
<gl-icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
</div>
{{ addedFilesLength }}
<div
v-tooltip
:title="modifiedTooltip"
data-container="body"
data-placement="left"
class="gl-mt-3 gl-mb-3"
>
<gl-icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
</div>
{{ modifiedFilesLength }}
</div>
</template>

View File

@ -14,6 +14,7 @@ export default {
},
computed: {
...mapGetters(['activeFile']),
...mapGetters('editor', ['activeFileEditor']),
activeFileEOL() {
return getFileEOL(this.activeFile.content);
},
@ -33,8 +34,10 @@ export default {
</gl-link>
</div>
<div>{{ activeFileEOL }}</div>
<div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div>
<div>{{ activeFile.fileLanguage }}</div>
<div v-if="activeFileIsText">
{{ activeFileEditor.editorRow }}:{{ activeFileEditor.editorColumn }}
</div>
<div>{{ activeFileEditor.fileLanguage }}</div>
</template>
<terminal-sync-status-safe />
</div>

View File

@ -22,6 +22,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
@ -49,6 +50,7 @@ export default {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
...mapState('editor', ['fileEditors']),
...mapState([
'viewer',
'panelResizing',
@ -67,6 +69,9 @@ export default {
'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
fileEditor() {
return getFileEditorOrDefault(this.fileEditors, this.file.path);
},
shouldHideEditor() {
return this.file && !this.file.loading && !isTextFile(this.file);
},
@ -80,10 +85,10 @@ export default {
return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr;
},
isEditorViewMode() {
return this.file.viewMode === FILE_VIEW_MODE_EDITOR;
return this.fileEditor.viewMode === FILE_VIEW_MODE_EDITOR;
},
isPreviewViewMode() {
return this.file.viewMode === FILE_VIEW_MODE_PREVIEW;
return this.fileEditor.viewMode === FILE_VIEW_MODE_PREVIEW;
},
editTabCSS() {
return {
@ -125,8 +130,7 @@ export default {
this.initEditor();
if (this.currentActivityView !== leftSidebarViews.edit.name) {
this.setFileViewMode({
file: this.file,
this.updateEditor({
viewMode: FILE_VIEW_MODE_EDITOR,
});
}
@ -134,8 +138,7 @@ export default {
},
currentActivityView() {
if (this.currentActivityView !== leftSidebarViews.edit.name) {
this.setFileViewMode({
file: this.file,
this.updateEditor({
viewMode: FILE_VIEW_MODE_EDITOR,
});
}
@ -195,13 +198,11 @@ export default {
'getFileData',
'getRawFileData',
'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileViewMode',
'removePendingTab',
'triggerFilesChange',
'addTempImage',
]),
...mapActions('editor', ['updateFileEditor']),
initEditor() {
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
return;
@ -284,19 +285,19 @@ export default {
// Handle Cursor Position
this.editor.onPositionChange((instance, e) => {
this.setEditorPosition({
this.updateEditor({
editorRow: e.position.lineNumber,
editorColumn: e.position.column,
});
});
this.editor.setPosition({
lineNumber: this.file.editorRow,
column: this.file.editorColumn,
lineNumber: this.fileEditor.editorRow,
column: this.fileEditor.editorColumn,
});
// Handle File Language
this.setFileLanguage({
this.updateEditor({
fileLanguage: this.model.language,
});
@ -354,6 +355,16 @@ export default {
const schema = this.getJsonSchemaForPath(this.file.path);
registerSchema(schema);
},
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
// when disposing. We want to ignore these by only capturing editor changes that happen to the currently active
// file.
if (!this.file.active) {
return;
}
this.updateFileEditor({ path: this.file.path, data });
},
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@ -369,7 +380,7 @@ export default {
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_EDITOR })"
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
>
{{ __('Edit') }}
</a>
@ -378,7 +389,7 @@ export default {
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
>{{ previewMode.previewTitle }}</a
>
</li>

View File

@ -5,7 +5,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import { stageKeys, commitActionTypes } from '../constants';
import service from '../services';
import eventHub from '../eventhub';
@ -242,7 +242,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
}
}
dispatch('triggerFilesChange');
dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath });
};
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>

View File

@ -164,26 +164,6 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
}
};
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (getters.activeFile) {
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
}
};
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
editorRow,
editorColumn,
});
}
};
export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const restoreOriginalFile = ({ dispatch, state, commit }, path) => {
const file = state.entries[path];
const isDestructiveDiscard = file.tempFile || file.prevPath;
@ -289,7 +269,7 @@ export const removePendingTab = ({ commit }, file) => {
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};
export const triggerFilesChange = () => {
export const triggerFilesChange = (ctx, payload = {}) => {
// Used in EE for file mirroring
eventHub.$emit('ide.files.change');
eventHub.$emit('ide.files.change', payload);
};

View File

@ -12,6 +12,8 @@ import fileTemplates from './modules/file_templates';
import paneModule from './modules/pane';
import clientsideModule from './modules/clientside';
import routerModule from './modules/router';
import editorModule from './modules/editor';
import { setupFileEditorsSync } from './modules/editor/setup';
Vue.use(Vuex);
@ -29,7 +31,14 @@ export const createStoreOptions = () => ({
rightPane: paneModule(),
clientside: clientsideModule(),
router: routerModule,
editor: editorModule,
},
});
export const createStore = () => new Vuex.Store(createStoreOptions());
export const createStore = () => {
const store = new Vuex.Store(createStoreOptions());
setupFileEditorsSync(store);
return store;
};

View File

@ -0,0 +1,19 @@
import * as types from './mutation_types';
/**
* Action to update the current file editor info at the given `path` with the given `data`
*
* @param {} vuex
* @param {{ path: String, data: any }} payload
*/
export const updateFileEditor = ({ commit }, payload) => {
commit(types.UPDATE_FILE_EDITOR, payload);
};
export const removeFileEditor = ({ commit }, path) => {
commit(types.REMOVE_FILE_EDITOR, path);
};
export const renameFileEditor = ({ commit }, payload) => {
commit(types.RENAME_FILE_EDITOR, payload);
};

View File

@ -0,0 +1,13 @@
import { getFileEditorOrDefault } from './utils';
export const activeFileEditor = (state, getters, rootState, rootGetters) => {
const { activeFile } = rootGetters;
if (!activeFile) {
return null;
}
const { path } = rootGetters.activeFile;
return getFileEditorOrDefault(state.fileEditors, path);
};

View File

@ -0,0 +1,12 @@
import * as actions from './actions';
import * as getters from './getters';
import state from './state';
import mutations from './mutations';
export default {
namespaced: true,
actions,
state,
mutations,
getters,
};

View File

@ -0,0 +1,3 @@
export const UPDATE_FILE_EDITOR = 'UPDATE_FILE_EDITOR';
export const REMOVE_FILE_EDITOR = 'REMOVE_FILE_EDITOR';
export const RENAME_FILE_EDITOR = 'RENAME_FILE_EDITOR';

View File

@ -0,0 +1,25 @@
import Vue from 'vue';
import * as types from './mutation_types';
import { getFileEditorOrDefault } from './utils';
export default {
[types.UPDATE_FILE_EDITOR](state, { path, data }) {
const editor = getFileEditorOrDefault(state.fileEditors, path);
Vue.set(state.fileEditors, path, Object.assign(editor, data));
},
[types.REMOVE_FILE_EDITOR](state, path) {
Vue.delete(state.fileEditors, path);
},
[types.RENAME_FILE_EDITOR](state, { path, newPath }) {
const existing = state.fileEditors[path];
// Gracefully do nothing if fileEditor isn't found.
if (!existing) {
return;
}
Vue.delete(state.fileEditors, path);
Vue.set(state.fileEditors, newPath, existing);
},
};

View File

@ -0,0 +1,19 @@
import eventHub from '~/ide/eventhub';
import { commitActionTypes } from '~/ide/constants';
const removeUnusedFileEditors = store => {
Object.keys(store.state.editor.fileEditors)
.filter(path => !store.state.entries[path])
.forEach(path => store.dispatch('editor/removeFileEditor', path));
};
export const setupFileEditorsSync = store => {
eventHub.$on('ide.files.change', ({ type, ...payload } = {}) => {
if (type === commitActionTypes.move) {
store.dispatch('editor/renameFileEditor', payload);
} else {
// The files have changed, but the specific change is not known.
removeUnusedFileEditors(store);
}
});
};

View File

@ -0,0 +1,8 @@
export default () => ({
// Object which represents a dictionary of filePath to editor specific properties, including:
// - fileLanguage
// - editorRow
// - editorCol
// - viewMode
fileEditors: {},
});

View File

@ -0,0 +1,11 @@
import { FILE_VIEW_MODE_EDITOR } from '../../../constants';
export const createDefaultFileEditor = () => ({
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
viewMode: FILE_VIEW_MODE_EDITOR,
});
export const getFileEditorOrDefault = (fileEditors, path) =>
fileEditors[path] || createDefaultFileEditor();

View File

@ -36,9 +36,6 @@ export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';

View File

@ -95,17 +95,6 @@ export default {
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(state.entries[file.path], {
fileLanguage,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(state.entries[file.path], {
editorRow,
editorColumn,
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
let diffMode = diffModes.replaced;
if (mrChange.new_file) {
@ -122,11 +111,6 @@ export default {
},
});
},
[types.SET_FILE_VIEWMODE](state, { file, viewMode }) {
Object.assign(state.entries[file.path], {
viewMode,
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path];

View File

@ -1,4 +1,4 @@
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants';
import { commitActionTypes } from '../constants';
import {
relativePathToAbsolute,
isAbsolute,
@ -25,10 +25,6 @@ export const dataStructure = () => ({
rawPath: '',
raw: '',
content: '',
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
viewMode: FILE_VIEW_MODE_EDITOR,
size: 0,
parentPath: null,
lastOpenedAt: 0,

View File

@ -116,7 +116,8 @@ export default {
<div data-testid="issuable-title" class="issue-title title">
<span class="issue-title-text" dir="auto">
<gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
>{{ issuable.title }}<gl-icon v-if="isIssuableUrlExternal" name="external-link"
>{{ issuable.title
}}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
/></gl-link>
</span>
</div>
@ -134,7 +135,9 @@ export default {
>{{ createdAt }}</span
>
{{ __('by') }}
<slot v-if="hasSlotContents('author')" name="author"></slot>
<gl-link
v-else
:data-user-id="authorId"
:data-username="author.username"
:data-name="author.name"

View File

@ -1,5 +1,5 @@
<script>
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@ -7,9 +7,11 @@ import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filte
import IssuableTabs from './issuable_tabs.vue';
import IssuableItem from './issuable_item.vue';
import { DEFAULT_SKELETON_COUNT } from '../constants';
export default {
components: {
GlLoadingIcon,
GlSkeletonLoading,
IssuableTabs,
FilteredSearchBar,
IssuableItem,
@ -88,7 +90,7 @@ export default {
required: false,
default: 20,
},
totalPages: {
totalItems: {
type: Number,
required: false,
default: 0,
@ -114,6 +116,19 @@ export default {
default: true,
},
},
computed: {
skeletonItemCount() {
const { totalItems, defaultPageSize, currentPage } = this;
const totalPages = Math.ceil(totalItems / defaultPageSize);
if (totalPages) {
return currentPage < totalPages
? defaultPageSize
: totalItems % defaultPageSize || defaultPageSize;
}
return DEFAULT_SKELETON_COUNT;
},
},
watch: {
urlParams: {
deep: true,
@ -157,7 +172,11 @@ export default {
@onSort="$emit('sort', $event)"
/>
<div class="issuables-holder">
<gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" />
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
<gl-skeleton-loading />
</li>
</ul>
<ul
v-if="!issuablesLoading && issuables.length"
class="content-list issuable-list issues-list"
@ -172,6 +191,9 @@ export default {
<template #reference>
<slot name="reference" :issuable="issuable"></slot>
</template>
<template #author>
<slot name="author" :author="issuable.author"></slot>
</template>
<template #status>
<slot name="status" :issuable="issuable"></slot>
</template>
@ -181,7 +203,7 @@ export default {
<gl-pagination
v-if="showPaginationControls"
:per-page="defaultPageSize"
:total-items="totalPages"
:total-items="totalItems"
:value="currentPage"
:prev-page="previousPage"
:next-page="nextPage"

View File

@ -47,3 +47,5 @@ export const AvailableSortOptions = [
];
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_SKELETON_COUNT = 5;

View File

@ -1,5 +0,0 @@
import initIssuablesList from '~/issues_list';
document.addEventListener('DOMContentLoaded', () => {
initIssuablesList();
});

View File

@ -1,8 +1,21 @@
<script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
} from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlForm,
GlFormGroup,
GlFormInput,
@ -17,6 +30,24 @@ export default {
type: String,
required: true,
},
templates: {
type: Array,
required: false,
default: null,
},
currentTemplate: {
type: Object,
required: false,
default: null,
},
},
computed: {
dropdownLabel() {
return this.currentTemplate ? this.currentTemplate.name : __('None');
},
hasTemplates() {
return this.templates?.length > 0;
},
},
mounted() {
this.preSelect();
@ -30,6 +61,9 @@ export default {
this.$refs.title.$el.select();
});
},
onChangeTemplate(template) {
this.$emit('changeTemplate', template || null);
},
onUpdate(field, value) {
const payload = {
title: this.title,
@ -58,6 +92,29 @@ export default {
/>
</gl-form-group>
<gl-form-group
v-if="hasTemplates"
key="template"
:label="__('Description template')"
:label-for="getId('control', 'template')"
>
<gl-dropdown :text="dropdownLabel">
<gl-dropdown-item key="none" @click="onChangeTemplate(null)">
{{ __('None') }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="template in templates"
:key="template.key"
@click="onChangeTemplate(template)"
>
{{ template.name }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-form-group
key="description"
:label="__('Goal of the changes and what reviewers should be aware of')"

View File

@ -22,6 +22,8 @@ export default {
data() {
return {
clearStorage: false,
currentTemplate: null,
mergeRequestTemplates: null,
mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath,
@ -61,6 +63,13 @@ export default {
onSecondary() {
this.hide();
},
onChangeTemplate(template) {
this.currentTemplate = template;
const description = this.currentTemplate ? this.currentTemplate.content : '';
const mergeRequestMeta = { ...this.mergeRequestMeta, description };
this.onUpdateSettings(mergeRequestMeta);
},
onUpdateSettings(mergeRequestMeta) {
this.mergeRequestMeta = { ...mergeRequestMeta };
},
@ -91,7 +100,10 @@ export default {
ref="editMetaControls"
:title="mergeRequestMeta.title"
:description="mergeRequestMeta.description"
:templates="mergeRequestTemplates"
:current-template="currentTemplate"
@updateSettings="onUpdateSettings"
@changeTemplate="onChangeTemplate"
/>
</gl-modal>
</template>

View File

@ -60,8 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
def update_registration
return redirect_to new_user_registration_path unless current_user
user_params = params.require(:user).permit(:role, :setup_for_company)
result = ::Users::SignupService.new(current_user, user_params).execute
result = ::Users::SignupService.new(current_user, update_registration_params).execute
if result[:status] == :success
if ::Gitlab.com? && show_onboarding_issues_experiment?
@ -164,6 +163,10 @@ class RegistrationsController < Devise::RegistrationsController
params.require(:user).permit(:username, :email, :name, :first_name, :last_name, :password)
end
def update_registration_params
params.require(:user).permit(:role, :setup_for_company)
end
def resource_name
:user
end

View File

@ -153,10 +153,8 @@ class IssuableFinder
end
def row_count
fast_fail = Feature.enabled?(:soft_fail_count_by_state, params.group || params.project)
Gitlab::IssuablesCountForState
.new(self, nil, fast_fail: fast_fail)
.new(self, nil, fast_fail: true)
.for_state_or_opened(params[:state])
end

View File

@ -28,7 +28,6 @@
= f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
= render_if_exists 'devise/shared/email_opted_in', f: f
%div
- if show_recaptcha_sign_up?
= recaptcha_tags

View File

@ -28,7 +28,6 @@
= f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
= render_if_exists 'devise/shared/email_opted_in', f: f
%div
- if show_recaptcha_sign_up?
= recaptcha_tags

View File

@ -0,0 +1,5 @@
---
title: Gracefully degrade when counting takes too long for a filtered search
merge_request: 46350
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Upgrade fog-google to v1.11.0
merge_request: 46648
author:
type: fixed

View File

@ -1,7 +1,7 @@
---
name: soft_fail_count_by_state
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44184
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263222
name: jira_issues_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45678
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273726
type: development
group: group::source code
group: group::ecosystem
default_enabled: false

View File

@ -6,7 +6,7 @@
gitlab-com: true
packages: [Starter, Premium, Ultimate]
url: https://www.youtube.com/watch?v=31pNKjenlJY&feature=emb_title
image_url: http://i3.ytimg.com/vi/31pNKjenlJY/maxresdefault.jpg
image_url: https://img.youtube.com/vi/31pNKjenlJY/hqdefault.jpg
published_at: 2020-07-22
release: 13.2
- title: Container Host Monitoring and Blocking
@ -16,7 +16,7 @@
gitlab-com: true
packages: [All]
url: https://www.youtube.com/watch?v=WxBzBz76FxU&feature=youtu.be
image_url: http://i3.ytimg.com/vi/WxBzBz76FxU/hqdefault.jpg
image_url: https://img.youtube.com/vi/WxBzBz76FxU/hqdefault.jpg
published_at: 2020-07-22
release: 13.2
- title: Official GitLab-Figma Plugin

View File

@ -6,7 +6,7 @@
gitlab-com: true
packages: [Ultimate]
url: https://www.youtube.com/watch?v=3wdWMDRLdp4
image_url: http://i3.ytimg.com/vi/3wdWMDRLdp4/hqdefault.jpg
image_url: https://img.youtube.com/vi/3wdWMDRLdp4/hqdefault.jpg
published_at: 2020-08-22
release: 13.3
- title: Create a matrix of jobs using a simple syntax

View File

@ -46,7 +46,7 @@
gitlab-com: true
packages: [starter, premium, ultimate]
url: https://www.youtube.com/embed/1FBRaBQTQZk
image_url: http://i3.ytimg.com/vi/1FBRaBQTQZk/maxresdefault.jpg
image_url: https://img.youtube.com/vi/1FBRaBQTQZk/hqdefault.jpg
published_at: 2020-09-22
release: 13.4
- title: Quick navigation using the Search bar

View File

@ -124,8 +124,6 @@ The following metrics can be controlled by feature flags:
|:---------------------------------------------------------------|:-------------------------------------------------------------------|
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
| `gitlab_issuable_fast_count_by_state_total` | `soft_fail_count_by_state` |
| `gitlab_issuable_fast_count_by_state_failures_total` | `soft_fail_count_by_state` |
## Sidekiq metrics

View File

@ -307,8 +307,12 @@ by testing the following commands:
```shell
sudo mkdir /gitlab-nfs/test-dir
sudo chown git /gitlab-nfs/test-dir
sudo chgrp gitlab-www /gitlab-nfs/test-dir
sudo chgrp root /gitlab-nfs/test-dir
sudo chmod 0700 /gitlab-nfs/test-dir
sudo chgrp gitlab-www /gitlab-nfs/test-dir
sudo chmod 0751 /gitlab-nfs/test-dir
sudo chgrp git /gitlab-nfs/test-dir
sudo chmod 2770 /gitlab-nfs/test-dir
sudo chmod 2755 /gitlab-nfs/test-dir
sudo -u git mkdir /gitlab-nfs/test-dir/test2
sudo -u git chmod 2755 /gitlab-nfs/test-dir/test2

View File

@ -1,6 +1,6 @@
---
stage: none
group: unassigned
stage: Enablement
group: Memory
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---

View File

@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5 TB of data. Although this

View File

@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5 TB of data. Although this

View File

@ -356,6 +356,13 @@ are supported and can be added if needed.
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5TB of data. Although this

View File

@ -1058,6 +1058,13 @@ Refer to your preferred Load Balancer's documentation for further guidance.
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5 TB of data. Although this

View File

@ -1335,6 +1335,13 @@ To configure the Sentinel Queues server:
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5 TB of data. Although this

View File

@ -1057,6 +1057,13 @@ Refer to your preferred Load Balancer's documentation for further guidance.
## Configure Gitaly
NOTE: **Note:**
[Gitaly Cluster](../gitaly/praefect.md) support
for the Reference Architectures is being
worked on as a [collaborative effort](https://gitlab.com/gitlab-org/quality/reference-architectures/-/issues/1) between the Quality Engineering and Gitaly teams. When this component has been verified
some Architecture specs will likely change as a result to support the new
and improved designed.
[Gitaly](../gitaly/index.md) server node requirements are dependent on data,
specifically the number of projects and those projects' sizes. It's recommended
that a Gitaly server node stores no more than 5 TB of data. Although this

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ addressed.
## How to create an A/B test
### Implementation
### Implement the experiment
1. Add the experiment to the `Gitlab::Experimentation::EXPERIMENTS` hash in [`experimentation.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib%2Fgitlab%2Fexperimentation.rb):
@ -50,7 +50,7 @@ addressed.
# Add your experiment here:
signup_flow: {
environment: ::Gitlab.dev_env_or_com?, # Target environment, defaults to enabled for development and GitLab.com
tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
tracking_category: 'Growth::Activation::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
}
}.freeze
```
@ -111,8 +111,131 @@ addressed.
end
```
1. Track necessary events. See the [product analytics guide](../product_analytics/index.md) for details.
1. After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) in the
### Implement the tracking events
To determine whether the experiment is a success or not, we must implement tracking events
to acquire data for analyzing. We can send events to Snowplow via either the backend or frontend.
Read the [product analytics guide](../product_analytics/index.md) for more details.
#### Track backend events
The framework provides the following helper method that is available in controllers:
```ruby
before_action do
track_experiment_event(:signup_flow, 'action', 'value')
end
```
Which can be tested as follows:
```ruby
context 'when the experiment is active and the user is in the experimental group' do
before do
stub_experiment(signup_flow: true)
stub_experiment_for_user(signup_flow: true)
end
it 'tracks an event', :snowplow do
subject
expect_snowplow_event(
category: 'Growth::Activation::Experiment::SignUpFlow',
action: 'action',
label: 'value',
label: 'experimentation_subject_id',
property: 'experimental_group'
)
end
end
```
#### Track frontend events
The framework provides the following helper method that is available in controllers:
```ruby
before_action do
push_frontend_experiment(:signup_flow)
frontend_experimentation_tracking_data(:signup_flow, 'action', 'value')
end
```
This pushes tracking data to `gon.experiments` and `gon.tracking_data`.
```ruby
expect(Gon.experiments['signupFlow']).to eq(true)
expect(Gon.tracking_data).to eq(
{
category: 'Growth::Activation::Experiment::SignUpFlow',
action: 'action',
value: 'value',
label: 'experimentation_subject_id',
property: 'experimental_group'
}
)
```
Which can then be used for tracking as follows:
```javascript
import { isExperimentEnabled } from '~/lib/utils/experimentation';
import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
const signupFlowExperimentEnabled = isExperimentEnabled('signupFlow');
if (signupFlowExperimentEnabled && gon.tracking_data) {
const { category, action, ...data } = gon.tracking_data;
Tracking.event(category, action, data);
}
}
```
Which can be tested in Jest as follows:
```javascript
import { withGonExperiment } from 'helpers/experimentation_helper';
import Tracking from '~/tracking';
describe('event tracking', () => {
describe('with tracking data', () => {
withGonExperiment('signupFlow');
beforeEach(() => {
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
gon.tracking_data = {
category: 'Growth::Activation::Experiment::SignUpFlow',
action: 'action',
value: 'value',
label: 'experimentation_subject_id',
property: 'experimental_group'
};
});
it('should track data', () => {
performAction()
expect(Tracking.event).toHaveBeenCalledWith(
'Growth::Activation::Experiment::SignUpFlow',
'action',
{
value: 'value',
label: 'experimentation_subject_id',
property: 'experimental_group'
},
);
});
});
});
```
### Enable the experiment
After all merge requests have been merged, use [`chatops`](../../ci/chatops/README.md) in the
[appropriate channel](../feature_flags/controls.md#communicate-the-change) to start the experiment for 10% of the users.
The feature flag should have the name of the experiment with the `_experiment_percentage` suffix appended.
For visibility, please also share any commands run against production in the `#s_growth` channel:
@ -127,9 +250,39 @@ For visibility, please also share any commands run against production in the `#s
/chatops run feature delete signup_flow_experiment_percentage
```
### Tests and test helpers
### Testing and test helpers
Use the following in Jest to test the experiment is enabled.
#### RSpec
Use the folowing in RSpec to mock the experiment:
```ruby
context 'when the experiment is active' do
before do
stub_experiment(signup_flow: true)
end
context 'when the user is in the experimental group' do
before do
stub_experiment_for_user(signup_flow: true)
end
it { is_expected.to do_experimental_thing }
end
context 'when the user is in the control group' do
before do
stub_experiment_for_user(signup_flow: false)
end
it { is_expected.to do_control_thing }
end
end
```
#### Jest
Use the following in Jest to mock the experiment:
```javascript
import { withGonExperiment } from 'helpers/experimentation_helper';

View File

@ -340,10 +340,10 @@ These results can also be placed into a PostgreSQL database by setting the
`RSPEC_PROFILING_POSTGRES_URL` variable. This is used to profile the test suite
when running in the CI environment.
We store these results also when running CI jobs on the default branch on
`gitlab.com`. Statistics of these profiling data are [available
online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For example,
you can find which tests take longest to run or which execute the most
We store these results also when running nightly scheduled CI jobs on the
default branch on `gitlab.com`. Statistics of these profiling data are
[available online](https://gitlab-org.gitlab.io/rspec_profiling_stats/). For
example, you can find which tests take longest to run or which execute the most
queries. This can be handy for optimizing our tests or identifying performance
issues in our code.

View File

@ -148,6 +148,7 @@ GitLab version | Minimum PostgreSQL version
-|-
10.0 | 9.6
13.0 | 11
13.6 | 12
You must also ensure the `pg_trgm` and `btree_gist` extensions are [loaded into every
GitLab database](postgresql_extensions.html).

View File

@ -151,6 +151,15 @@ dnf install gitlab-ee-12.0.12-ee.0.el8
zypper install gitlab-ee=12.0.12-ee.0
```
To identify the GitLab version number in your package manager, run the following commands:
```shell
# apt-cache (Ubuntu/Debian)
sudo apt-cache madison gitlab-ee
# yum (RHEL/CentOS 6 and 7)
yum --showduplicates list gitlab-ee
```
## Patch releases
Patch releases **only include bug fixes** for the current stable released version of

View File

@ -23,7 +23,10 @@ you can run fuzz tests as part your CI/CD workflow.
- SOAP
- GraphQL
- Form bodies, JSON, or XML
- An OpenAPI definition, or HTTP Archive (HAR) of requests to test
- One of the following assets to provide APIs to test:
- OpenAPI v2 API definition
- HTTP Archive (HAR) of API requests to test
- Postman Collection v2.0 or v2.1
## When fuzzing scans run
@ -48,15 +51,17 @@ changes, other pipelines, or other scanners) during a scan could cause inaccurat
## Configuration
There are two ways to perform scans. See the configuration section for the one you wish to use:
There are three ways to perform scans. See the configuration section for the one you wish to use:
- [OpenAPI v2 specification](#openapi-specification)
- [HTTP Archive (HAR)](#http-archive-har)
- [Postman Collection v2.0 or v2.1](#postman-collection)
Examples of both configurations can be found here:
- [Example OpenAPI v2 specification project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing-example/-/tree/openapi)
- [Example HTTP Archive (HAR) project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing-example/-/tree/har)
- [Example Postman Collection project](https://gitlab.com/gitlab-org/security-products/demos/api-fuzzing/postman-collection/)
### OpenAPI Specification
@ -229,6 +234,97 @@ DANGER: **Warning:**
the API can, it may also trigger bugs in the API. This includes actions like modifying and deleting
data. Only run fuzzing against a test server.
### Postman Collection
The [Postman API Client](https://www.postman.com/product/api-client/) is a popular tool that
developers and testers use to call various types of APIs. The API definitions
[can be exported as a Postman Collection file](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/#exporting-postman-data)
for use with API Fuzzing. When exporting, make sure to select a supported version of Postman
Collection: v2.0 or v2.1.
When used with GitLab's API fuzzer, Postman Collections must contain definitions of the web API to
test with valid data. The API fuzzer extracts all the API definitions and uses them to perform
testing.
DANGER: **Warning:**
Postman Collection files may contain sensitive information such as authentication tokens, API keys,
and session cookies. We recommend that you review the Postman Collection file contents before adding
them to a repository.
Follow these steps to configure API fuzzing to use a Postman Collection file that provides
information about the target API to test:
1. To use API fuzzing, you must [include](../../../ci/yaml/README.md#includetemplate)
the [`API-Fuzzing.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml)
that's provided as part of your GitLab installation. To do so, add the following to your
`.gitlab-ci.yml` file:
```yaml
include:
- template: API-Fuzzing.gitlab-ci.yml
```
1. Add the configuration file [`gitlab-api-fuzzing-config.yml`](https://gitlab.com/gitlab-org/security-products/analyzers/api-fuzzing/-/blob/master/gitlab-api-fuzzing-config.yml)
to your repository's root as `.gitlab-api-fuzzing.yml`.
1. The [configuration file](#configuration-files) has several testing profiles defined with varying
amounts of fuzzing. We recommend that you start with the `Quick-10` profile. Testing with this
profile completes quickly, allowing for easier configuration validation.
Provide the profile by adding the `FUZZAPI_PROFILE` variable to your `.gitlab-ci.yml` file,
substituting `Quick-10` for the profile you choose:
```yaml
include:
- template: API-Fuzzing.gitlab-ci.yml
variables:
FUZZAPI_PROFILE: Quick-10
```
1. Add the `FUZZAPI_POSTMAN_COLLECTION` variable and set it to the Postman Collection's location:
```yaml
include:
- template: API-Fuzzing.gitlab-ci.yml
variables:
FUZZAPI_PROFILE: Quick-10
FUZZAPI_POSTMAN_COLLECTION: postman-collection_serviceA.json
```
1. The target API instance's base URL is also required. Provide it by using the `FUZZAPI_TARGET_URL`
variable or an `environment_url.txt` file.
Adding the URL in an `environment_url.txt` file at your project's root is great for testing in
dynamic environments. To run API fuzzing against an app dynamically created during a GitLab CI/CD
pipeline, have the app persist its domain in an `environment_url.txt` file. API fuzzing
automatically parses that file to find its scan target. You can see an
[example of this in our Auto DevOps CI YAML](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml).
Here's an example of using `FUZZAPI_TARGET_URL`:
```yaml
include:
- template: API-Fuzzing.gitlab-ci.yml
variables:
FUZZAPI_PROFILE: Quick-10
FUZZAPI_POSTMAN_COLLECTION: postman-collection_serviceA.json
FUZZAPI_TARGET_URL: http://test-deployment/
```
This is a minimal configuration for API Fuzzing. From here you can:
- [Run your first scan](#running-your-first-scan).
- [Add authentication](#authentication).
- Learn how to [handle false positives](#handling-false-positives).
DANGER: **Warning:**
**NEVER** run fuzz testing against a production server. Not only can it perform *any* function that
the API can, it may also trigger bugs in the API. This includes actions like modifying and deleting
data. Only run fuzzing against a test server.
### Authentication
Authentication is handled by providing the authentication token as a header or cookie. You can
@ -398,6 +494,7 @@ increases as the numbers go up. To use a configuration file, add it to your repo
| `FUZZAPI_REPORT` |Scan report filename. Defaults to `gl-api_fuzzing-report.xml`. |
|[`FUZZAPI_OPENAPI`](#openapi-specification)|OpenAPI specification file or URL. |
|[`FUZZAPI_HAR`](#http-archive-har)|HTTP Archive (HAR) file. |
|[`FUZZAPI_POSTMAN_COLLECTION`](#postman-collection)|Postman Collection file. |
|[`FUZZAPI_OVERRIDES_FILE`](#overrides) |Path to a JSON file containing overrides. |
|[`FUZZAPI_OVERRIDES_ENV`](#overrides) |JSON string containing headers to override. |
|[`FUZZAPI_OVERRIDES_CMD`](#overrides) |Overrides command. |

View File

@ -61,11 +61,17 @@ apifuzzer_fuzz:
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $FUZZAPI_HAR == null && $FUZZAPI_OPENAPI == null
when: never
- if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
script:
#
# Validate options
- |
if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI$FUZZAPI_POSTMAN_COLLECTION" == "" ]; then \
echo "Error: One of FUZZAPI_HAR, FUZZAPI_OPENAPI, or FUZZAPI_POSTMAN_COLLECTION must be provided."; \
echo "See https://docs.gitlab.com/ee/user/application_security/api_fuzzing/ for information on how to configure API Fuzzing."; \
exit 1; \
fi
#
# Run user provided pre-script
- sh -c "$FUZZAPI_PRE_SCRIPT"
#
@ -96,8 +102,6 @@ apifuzzer_fuzz_dnd:
- if: $API_FUZZING_DISABLED_FOR_DEFAULT_BRANCH &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $FUZZAPI_HAR == null && $FUZZAPI_OPENAPI == null
when: never
- if: $GITLAB_FEATURES =~ /\bapi_fuzzing\b/
services:
- docker:19.03.12-dind
@ -142,7 +146,7 @@ apifuzzer_fuzz_dnd:
# Start worker container if provided
- |
if [ "$FUZZAPI_D_WORKER_IMAGE" != "" ]; then \
echo "Starting worker image $FUZZAPI_D_WORKER_IMAGE" \
echo "Starting worker image $FUZZAPI_D_WORKER_IMAGE"; \
docker run \
--name worker \
--network $FUZZAPI_D_NETWORK \
@ -153,6 +157,7 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_REPORT \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
-e FUZZAPI_TARGET_URL \
-e FUZZAPI_OVERRIDES_FILE \
-e FUZZAPI_OVERRIDES_ENV \
@ -174,6 +179,11 @@ apifuzzer_fuzz_dnd:
# Start API Fuzzing provided worker if no other worker present
- |
if [ "$FUZZAPI_D_WORKER_IMAGE" == "" ]; then \
if [ "$FUZZAPI_HAR$FUZZAPI_OPENAPI$FUZZAPI_POSTMAN_COLLECTION" == "" ]; then \
echo "Error: One of FUZZAPI_HAR, FUZZAPI_OPENAPI, or FUZZAPI_POSTMAN_COLLECTION must be provided."; \
echo "See https://docs.gitlab.com/ee/user/application_security/api_fuzzing/ for information on how to configure API Fuzzing."; \
exit 1; \
fi; \
docker run \
--name worker \
--network $FUZZAPI_D_NETWORK \
@ -185,6 +195,7 @@ apifuzzer_fuzz_dnd:
-e FUZZAPI_REPORT \
-e FUZZAPI_HAR \
-e FUZZAPI_OPENAPI \
-e FUZZAPI_POSTMAN_COLLECTION \
-e FUZZAPI_TARGET_URL \
-e FUZZAPI_OVERRIDES_FILE \
-e FUZZAPI_OVERRIDES_ENV \

View File

@ -1062,16 +1062,6 @@ msgstr ""
msgid "0t1DgySidms"
msgstr ""
msgid "1 %{type} addition"
msgid_plural "%{count} %{type} additions"
msgstr[0] ""
msgstr[1] ""
msgid "1 %{type} modification"
msgid_plural "%{count} %{type} modifications"
msgstr[0] ""
msgstr[1] ""
msgid "1 Day"
msgid_plural "%d Days"
msgstr[0] ""
@ -9109,6 +9099,9 @@ msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
msgstr ""
msgid "Description template"
msgstr ""
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
msgstr ""
@ -9825,6 +9818,9 @@ msgstr ""
msgid "Email the pipelines status to a list of recipients."
msgstr ""
msgid "Email updates (optional)"
msgstr ""
msgid "EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies."
msgstr ""
@ -13638,7 +13634,7 @@ msgstr ""
msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)"
msgstr ""
msgid "I'd like to receive updates via email about GitLab"
msgid "I'd like to receive updates about GitLab via email"
msgstr ""
msgid "ID"
@ -14403,6 +14399,9 @@ msgstr ""
msgid "Integrations|Connection successful."
msgstr ""
msgid "Integrations|Create new issue in Jira"
msgstr ""
msgid "Integrations|Default settings are inherited from the group level."
msgstr ""
@ -14418,6 +14417,9 @@ msgstr ""
msgid "Integrations|Includes commit title and branch"
msgstr ""
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
msgstr ""
msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults."
msgstr ""
@ -14430,9 +14432,15 @@ msgstr ""
msgid "Integrations|Saving will update the default settings for all projects that are not using custom settings."
msgstr ""
msgid "Integrations|Search Jira issues"
msgstr ""
msgid "Integrations|Standard"
msgstr ""
msgid "Integrations|To keep this project going, create a new issue."
msgstr ""
msgid "Integrations|Update your projects on Packagist, the main Composer repository"
msgstr ""
@ -23310,6 +23318,9 @@ msgstr ""
msgid "Search Jira issues"
msgstr ""
msgid "Search a group"
msgstr ""
msgid "Search an environment spec"
msgstr ""

View File

@ -65,7 +65,7 @@ COPY VERSION ./ee/app/models/license.r[b] /home/gitlab/ee/app/models/
COPY ./lib/gitlab.rb /home/gitlab/lib/
COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/
COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/
RUN cd /home/gitlab/qa/ && gem install bundler:1.17.3 && bundle install --jobs=$(nproc) --retry=3 --without=development --quiet
RUN cd /home/gitlab/qa/ && bundle install --jobs=$(nproc) --retry=3 --without=development --quiet
COPY ./qa /home/gitlab/qa
ENTRYPOINT ["bin/test"]

View File

@ -175,4 +175,4 @@ DEPENDENCIES
timecop (~> 0.9.1)
BUNDLED WITH
1.17.3
2.1.4

View File

@ -5,8 +5,6 @@ export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS=${BUNDLE_INSTALL_FLAGS:-"--without=production development --jobs=$(nproc) --path=vendor --retry=3 --quiet"}
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
# This is for backwards compatibility for Gitaly
run_timed_command "gem install bundler:1.17.3"
bundle --version
run_timed_command "bundle install --clean ${BUNDLE_INSTALL_FLAGS}"
run_timed_command "bundle check"

View File

@ -46,6 +46,20 @@ RSpec.describe 'IDE user sees editor info', :js do
end
end
it 'persists position after rename' do
ide_open_file('README.md')
ide_set_editor_position(4, 10)
ide_open_file('files/js/application.js')
ide_rename_file('README.md', 'READING_RAINBOW.md')
ide_open_file('READING_RAINBOW.md')
within find('.ide-status-bar') do
expect(page).to have_content('4:10')
end
end
it 'persists position' do
ide_open_file('README.md')
ide_set_editor_position(4, 10)

View File

@ -1,75 +0,0 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { file } from '../../helpers';
import { removeWhitespace } from '../../../helpers/text_helper';
describe('Multi-file editor commit sidebar list collapsed', () => {
let vm;
let store;
beforeEach(() => {
store = createStore();
const Component = Vue.extend(listCollapsed);
vm = createComponentWithStore(Component, store, {
files: [
{
...file('file1'),
tempFile: true,
},
file('file2'),
],
iconName: 'staged',
title: 'Staged',
});
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
it('renders added & modified files count', () => {
expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1');
});
describe('addedFilesLength', () => {
it('returns an length of temp files', () => {
expect(vm.addedFilesLength).toBe(1);
});
});
describe('modifiedFilesLength', () => {
it('returns an length of modified files', () => {
expect(vm.modifiedFilesLength).toBe(1);
});
});
describe('addedFilesIconClass', () => {
it('includes multi-file-addition when addedFiles is not empty', () => {
expect(vm.addedFilesIconClass).toContain('multi-file-addition');
});
it('excludes multi-file-addition when addedFiles is empty', () => {
vm.files = [];
expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
});
});
describe('modifiedFilesClass', () => {
it('includes multi-file-modified when addedFiles is not empty', () => {
expect(vm.modifiedFilesClass).toContain('multi-file-modified');
});
it('excludes multi-file-modified when addedFiles is empty', () => {
vm.files = [];
expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
});
});
});

View File

@ -6,17 +6,21 @@ import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync
const TEST_FILE = {
name: 'lorem.md',
editorRow: 3,
editorColumn: 23,
fileLanguage: 'markdown',
content: 'abc\nndef',
permalink: '/lorem.md',
};
const TEST_FILE_EDITOR = {
fileLanguage: 'markdown',
editorRow: 3,
editorColumn: 23,
};
const TEST_EDITOR_POSITION = `${TEST_FILE_EDITOR.editorRow}:${TEST_FILE_EDITOR.editorColumn}`;
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ide/components/ide_status_list', () => {
let activeFileEditor;
let activeFile;
let store;
let wrapper;
@ -27,6 +31,14 @@ describe('ide/components/ide_status_list', () => {
getters: {
activeFile: () => activeFile,
},
modules: {
editor: {
namespaced: true,
getters: {
activeFileEditor: () => activeFileEditor,
},
},
},
});
wrapper = shallowMount(IdeStatusList, {
@ -38,6 +50,7 @@ describe('ide/components/ide_status_list', () => {
beforeEach(() => {
activeFile = TEST_FILE;
activeFileEditor = TEST_FILE_EDITOR;
});
afterEach(() => {
@ -47,8 +60,6 @@ describe('ide/components/ide_status_list', () => {
wrapper = null;
});
const getEditorPosition = file => `${file.editorRow}:${file.editorColumn}`;
describe('with regular file', () => {
beforeEach(() => {
createComponent();
@ -65,11 +76,11 @@ describe('ide/components/ide_status_list', () => {
});
it('shows file editor position', () => {
expect(wrapper.text()).toContain(getEditorPosition(TEST_FILE));
expect(wrapper.text()).toContain(TEST_EDITOR_POSITION);
});
it('shows file language', () => {
expect(wrapper.text()).toContain(TEST_FILE.fileLanguage);
expect(wrapper.text()).toContain(TEST_FILE_EDITOR.fileLanguage);
});
});
@ -81,7 +92,7 @@ describe('ide/components/ide_status_list', () => {
});
it('does not show file editor position', () => {
expect(wrapper.text()).not.toContain(getEditorPosition(TEST_FILE));
expect(wrapper.text()).not.toContain(TEST_EDITOR_POSITION);
});
});

View File

@ -55,7 +55,6 @@ describe('RepoEditor', () => {
beforeEach(() => {
const f = {
...file('file.txt'),
viewMode: FILE_VIEW_MODE_EDITOR,
content: 'hello world',
};
@ -92,6 +91,8 @@ describe('RepoEditor', () => {
});
const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder');
const changeViewMode = viewMode =>
store.dispatch('editor/updateFileEditor', { path: vm.file.path, data: { viewMode } });
describe('default', () => {
beforeEach(() => {
@ -409,7 +410,7 @@ describe('RepoEditor', () => {
describe('when files view mode is preview', () => {
beforeEach(done => {
jest.spyOn(vm.editor, 'updateDimensions').mockImplementation();
vm.file.viewMode = FILE_VIEW_MODE_PREVIEW;
changeViewMode(FILE_VIEW_MODE_PREVIEW);
vm.file.name = 'myfile.md';
vm.file.content = 'hello world';
@ -423,7 +424,7 @@ describe('RepoEditor', () => {
describe('when file view mode changes to editor', () => {
it('should update dimensions', () => {
vm.file.viewMode = FILE_VIEW_MODE_EDITOR;
changeViewMode(FILE_VIEW_MODE_EDITOR);
return vm.$nextTick().then(() => {
expect(vm.editor.updateDimensions).toHaveBeenCalled();

View File

@ -1,5 +1,6 @@
import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils';
import { commitActionTypes } from '~/ide/constants';
export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
@ -28,3 +29,17 @@ export const createEntriesFromPaths = paths =>
...entries,
};
}, {});
export const createTriggerChangeAction = payload => ({
type: 'triggerFilesChange',
...(payload ? { payload } : {}),
});
export const createTriggerRenamePayload = (path, newPath) => ({
type: commitActionTypes.move,
path,
newPath,
});
export const createTriggerRenameAction = (path, newPath) =>
createTriggerChangeAction(createTriggerRenamePayload(path, newPath));

View File

@ -7,7 +7,7 @@ import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
import { createRouter } from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file } from '../../helpers';
import { file, createTriggerRenameAction } from '../../helpers';
const ORIGINAL_CONTENT = 'original content';
const RELATIVE_URL_ROOT = '/gitlab';
@ -785,13 +785,19 @@ describe('IDE store file actions', () => {
});
describe('triggerFilesChange', () => {
const { payload: renamePayload } = createTriggerRenameAction('test', '123');
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('emits event that files have changed', () => {
return store.dispatch('triggerFilesChange').then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change');
it.each`
args | payload
${[]} | ${{}}
${[renamePayload]} | ${renamePayload}
`('emits event that files have changed (args=$args)', ({ args, payload }) => {
return store.dispatch('triggerFilesChange', ...args).then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change', payload);
});
});
});

View File

@ -19,7 +19,7 @@ import {
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
import * as types from '~/ide/stores/mutation_types';
import { file } from '../helpers';
import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub';
@ -522,7 +522,7 @@ describe('Multi-file store actions', () => {
'path',
store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }],
[{ type: 'stageChange', payload: 'path' }, { type: 'triggerFilesChange' }],
[{ type: 'stageChange', payload: 'path' }, createTriggerChangeAction()],
done,
);
});
@ -551,7 +551,7 @@ describe('Multi-file store actions', () => {
[{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }],
[
{ type: 'stageChange', payload: 'testFolder/entry-to-delete' },
{ type: 'triggerFilesChange' },
createTriggerChangeAction(),
],
done,
);
@ -614,7 +614,7 @@ describe('Multi-file store actions', () => {
testEntry.path,
store.state,
[{ type: types.DELETE_ENTRY, payload: testEntry.path }],
[{ type: 'stageChange', payload: testEntry.path }, { type: 'triggerFilesChange' }],
[{ type: 'stageChange', payload: testEntry.path }, createTriggerChangeAction()],
done,
);
});
@ -754,7 +754,7 @@ describe('Multi-file store actions', () => {
payload: origEntry,
},
],
[{ type: 'triggerFilesChange' }],
[createTriggerRenameAction('renamed', 'orig')],
done,
);
});
@ -767,7 +767,7 @@ describe('Multi-file store actions', () => {
{ path: 'orig', name: 'renamed' },
store.state,
[expect.objectContaining({ type: types.RENAME_ENTRY })],
[{ type: 'triggerFilesChange' }],
[createTriggerRenameAction('orig', 'renamed')],
done,
);
});

View File

@ -0,0 +1,36 @@
import testAction from 'helpers/vuex_action_helper';
import * as types from '~/ide/stores/modules/editor/mutation_types';
import * as actions from '~/ide/stores/modules/editor/actions';
import { createTriggerRenamePayload } from '../../../helpers';
describe('~/ide/stores/modules/editor/actions', () => {
describe('updateFileEditor', () => {
it('commits with payload', () => {
const payload = {};
testAction(actions.updateFileEditor, payload, {}, [
{ type: types.UPDATE_FILE_EDITOR, payload },
]);
});
});
describe('removeFileEditor', () => {
it('commits with payload', () => {
const payload = 'path/to/file.txt';
testAction(actions.removeFileEditor, payload, {}, [
{ type: types.REMOVE_FILE_EDITOR, payload },
]);
});
});
describe('renameFileEditor', () => {
it('commits with payload', () => {
const payload = createTriggerRenamePayload('test', 'test123');
testAction(actions.renameFileEditor, payload, {}, [
{ type: types.RENAME_FILE_EDITOR, payload },
]);
});
});
});

View File

@ -0,0 +1,31 @@
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as getters from '~/ide/stores/modules/editor/getters';
const TEST_PATH = 'test/path.md';
const TEST_FILE_EDITOR = {
...createDefaultFileEditor(),
editorRow: 7,
editorColumn: 8,
fileLanguage: 'markdown',
};
describe('~/ide/stores/modules/editor/getters', () => {
describe('activeFileEditor', () => {
it.each`
activeFile | fileEditors | expected
${null} | ${{}} | ${null}
${{}} | ${{}} | ${createDefaultFileEditor()}
${{ path: TEST_PATH }} | ${{}} | ${createDefaultFileEditor()}
${{ path: TEST_PATH }} | ${{ bogus: createDefaultFileEditor(), [TEST_PATH]: TEST_FILE_EDITOR }} | ${TEST_FILE_EDITOR}
`(
'with activeFile=$activeFile and fileEditors=$fileEditors',
({ activeFile, fileEditors, expected }) => {
const rootGetters = { activeFile };
const state = { fileEditors };
const result = getters.activeFileEditor(state, {}, {}, rootGetters);
expect(result).toEqual(expected);
},
);
});
});

View File

@ -0,0 +1,78 @@
import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils';
import * as types from '~/ide/stores/modules/editor/mutation_types';
import mutations from '~/ide/stores/modules/editor/mutations';
import { createTriggerRenamePayload } from '../../../helpers';
const TEST_PATH = 'test/path.md';
describe('~/ide/stores/modules/editor/mutations', () => {
describe(types.UPDATE_FILE_EDITOR, () => {
it('with path that does not exist, should initialize with default values', () => {
const state = { fileEditors: {} };
const data = { fileLanguage: 'markdown' };
mutations[types.UPDATE_FILE_EDITOR](state, { path: TEST_PATH, data });
expect(state.fileEditors).toEqual({
[TEST_PATH]: {
...createDefaultFileEditor(),
...data,
},
});
});
it('with existing path, should overwrite values', () => {
const state = {
fileEditors: {
foo: {},
[TEST_PATH]: { ...createDefaultFileEditor(), editorRow: 7, editorColumn: 7 },
},
};
mutations[types.UPDATE_FILE_EDITOR](state, {
path: TEST_PATH,
data: { fileLanguage: 'markdown' },
});
expect(state).toEqual({
fileEditors: {
foo: {},
[TEST_PATH]: {
...createDefaultFileEditor(),
editorRow: 7,
editorColumn: 7,
fileLanguage: 'markdown',
},
},
});
});
});
describe(types.REMOVE_FILE_EDITOR, () => {
it.each`
fileEditors | path | expected
${{}} | ${'does/not/exist.txt'} | ${{}}
${{ foo: {}, [TEST_PATH]: {} }} | ${TEST_PATH} | ${{ foo: {} }}
`('removes file $path', ({ fileEditors, path, expected }) => {
const state = { fileEditors };
mutations[types.REMOVE_FILE_EDITOR](state, path);
expect(state).toEqual({ fileEditors: expected });
});
});
describe(types.RENAME_FILE_EDITOR, () => {
it.each`
fileEditors | payload | expected
${{ foo: {} }} | ${createTriggerRenamePayload('does/not/exist', 'abc')} | ${{ foo: {} }}
${{ foo: { a: 1 }, bar: {} }} | ${createTriggerRenamePayload('foo', 'abc/def')} | ${{ 'abc/def': { a: 1 }, bar: {} }}
`('renames fileEditor at $payload', ({ fileEditors, payload, expected }) => {
const state = { fileEditors };
mutations[types.RENAME_FILE_EDITOR](state, payload);
expect(state).toEqual({ fileEditors: expected });
});
});
});

View File

@ -0,0 +1,44 @@
import Vuex from 'vuex';
import eventHub from '~/ide/eventhub';
import { createStoreOptions } from '~/ide/stores';
import { setupFileEditorsSync } from '~/ide/stores/modules/editor/setup';
import { createTriggerRenamePayload } from '../../../helpers';
describe('~/ide/stores/modules/editor/setup', () => {
let store;
beforeEach(() => {
store = new Vuex.Store(createStoreOptions());
store.state.entries = {
foo: {},
bar: {},
};
store.state.editor.fileEditors = {
foo: {},
bizz: {},
};
setupFileEditorsSync(store);
});
it('when files change is emitted, removes unused fileEditors', () => {
eventHub.$emit('ide.files.change');
expect(store.state.entries).toEqual({
foo: {},
bar: {},
});
expect(store.state.editor.fileEditors).toEqual({
foo: {},
});
});
it('when files rename is emitted, renames fileEditor', () => {
eventHub.$emit('ide.files.change', createTriggerRenamePayload('foo', 'foo_new'));
expect(store.state.editor.fileEditors).toEqual({
foo_new: {},
bizz: {},
});
});
});

View File

@ -1,6 +1,5 @@
import mutations from '~/ide/stores/mutations/file';
import { createStore } from '~/ide/stores';
import { FILE_VIEW_MODE_PREVIEW } from '~/ide/constants';
import { file } from '../../helpers';
describe('IDE store file mutations', () => {
@ -532,17 +531,6 @@ describe('IDE store file mutations', () => {
});
});
describe('SET_FILE_VIEWMODE', () => {
it('updates file view mode', () => {
mutations.SET_FILE_VIEWMODE(localState, {
file: localFile,
viewMode: FILE_VIEW_MODE_PREVIEW,
});
expect(localFile.viewMode).toBe(FILE_VIEW_MODE_PREVIEW);
});
});
describe('ADD_PENDING_TAB', () => {
beforeEach(() => {
const f = { ...file('openFile'), path: 'openFile', active: true, opened: true };

View File

@ -261,6 +261,24 @@ describe('IssuableItem', () => {
expect(authorEl.text()).toBe(mockAuthor.name);
});
it('renders issuable author info via slot', () => {
const wrapperWithAuthorSlot = createComponent({
issuableSymbol: '#',
issuable: mockIssuable,
slots: {
reference: `
<span class="js-author">${mockAuthor.name}</span>
`,
},
});
const authorEl = wrapperWithAuthorSlot.find('.js-author');
expect(authorEl.exists()).toBe(true);
expect(authorEl.text()).toBe(mockAuthor.name);
wrapperWithAuthorSlot.destroy();
});
it('renders gl-label component for each label present within `issuable` prop', () => {
const labelsEl = wrapper.findAll(GlLabel);

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
@ -34,6 +34,31 @@ describe('IssuableListRoot', () => {
wrapper.destroy();
});
describe('computed', () => {
describe('skeletonItemCount', () => {
it.each`
totalItems | defaultPageSize | currentPage | returnValue
${100} | ${20} | ${1} | ${20}
${105} | ${20} | ${6} | ${5}
${7} | ${20} | ${1} | ${7}
${0} | ${20} | ${1} | ${5}
`(
'returns $returnValue when totalItems is $totalItems, defaultPageSize is $defaultPageSize and currentPage is $currentPage',
async ({ totalItems, defaultPageSize, currentPage, returnValue }) => {
wrapper.setProps({
totalItems,
defaultPageSize,
currentPage,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.skeletonItemCount).toBe(returnValue);
},
);
});
});
describe('watch', () => {
describe('urlParams', () => {
it('updates window URL reflecting props within `urlParams`', async () => {
@ -111,7 +136,7 @@ describe('IssuableListRoot', () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(wrapper.vm.skeletonItemCount);
});
it('renders issuable-item component for each item within `issuables` array', () => {
@ -139,7 +164,7 @@ describe('IssuableListRoot', () => {
it('renders gl-pagination when `showPaginationControls` prop is true', async () => {
wrapper.setProps({
showPaginationControls: true,
totalPages: 10,
totalItems: 10,
});
await wrapper.vm.$nextTick();

View File

@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlFormInput, GlFormTextarea } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
import { mergeRequestMeta } from '../mock_data';
import { mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
let wrapper;
@ -19,6 +19,8 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
propsData: {
title,
description,
templates: mergeRequestTemplates,
currentTemplate: null,
...propsData,
},
});
@ -31,6 +33,10 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
};
const findGlFormInputTitle = () => wrapper.find(GlFormInput);
const findGlDropdownDescriptionTemplate = () => wrapper.find(GlDropdown);
const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findDropdownItemByIndex = index => findAllDropdownItems().at(index);
const findGlFormTextAreaDescription = () => wrapper.find(GlFormTextarea);
beforeEach(() => {
@ -49,6 +55,10 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
expect(findGlFormInputTitle().exists()).toBe(true);
});
it('renders the description template dropdown', () => {
expect(findGlDropdownDescriptionTemplate().exists()).toBe(true);
});
it('renders the description input', () => {
expect(findGlFormTextAreaDescription().exists()).toBe(true);
});
@ -65,6 +75,11 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
expect(mockGlFormInputTitleInstance.$el.select).toHaveBeenCalled();
});
it('renders a GlDropdownItem per template plus one (for the starting none option)', () => {
expect(findDropdownItemByIndex(0).text()).toBe('None');
expect(findAllDropdownItems().length).toBe(mergeRequestTemplates.length + 1);
});
describe('when inputs change', () => {
const storageKey = 'sse-merge-request-meta-local-storage-editable';
@ -84,4 +99,17 @@ describe('~/static_site_editor/components/edit_meta_controls.vue', () => {
expect(wrapper.emitted('updateSettings')[0][0]).toMatchObject(newSettings);
});
});
describe('when templates change', () => {
it.each`
index | value
${0} | ${null}
${1} | ${mergeRequestTemplates[0]}
${2} | ${mergeRequestTemplates[1]}
`('emits a change template event when $index is clicked', ({ index, value }) => {
findDropdownItemByIndex(index).vm.$emit('click');
expect(wrapper.emitted('changeTemplate')[0][0]).toBe(value);
});
});
});

View File

@ -5,7 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
import EditMetaControls from '~/static_site_editor/components/edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '~/static_site_editor/constants';
import { sourcePath, mergeRequestMeta } from '../mock_data';
import { sourcePath, mergeRequestMeta, mergeRequestTemplates } from '../mock_data';
describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
useLocalStorageSpy();
@ -15,12 +15,13 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
let mockEditMetaControlsInstance;
const { title, description } = mergeRequestMeta;
const buildWrapper = (propsData = {}) => {
const buildWrapper = (propsData = {}, data = {}) => {
wrapper = shallowMount(EditMetaModal, {
propsData: {
sourcePath,
...propsData,
},
data: () => data,
});
};
@ -51,7 +52,12 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
});
it('initializes initial merge request meta with local storage data', async () => {
const localStorageMeta = { title: 'stored title', description: 'stored description' };
const localStorageMeta = {
title: 'stored title',
description: 'stored description',
templates: null,
currentTemplate: null,
};
findLocalStorageSync().vm.$emit('input', localStorageMeta);
@ -80,6 +86,14 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
expect(findEditMetaControls().props('description')).toBe(description);
});
it('forwards the templates prop', () => {
expect(findEditMetaControls().props('templates')).toBe(null);
});
it('forwards the currentTemplate prop', () => {
expect(findEditMetaControls().props('currentTemplate')).toBe(null);
});
describe('when save button is clicked', () => {
beforeEach(() => {
findGlModal().vm.$emit('primary', mergeRequestMeta);
@ -94,6 +108,36 @@ describe('~/static_site_editor/components/edit_meta_modal.vue', () => {
});
});
describe('when templates exist', () => {
const template1 = mergeRequestTemplates[0];
beforeEach(() => {
buildWrapper({}, { templates: mergeRequestTemplates, currentTemplate: null });
});
it('sets the currentTemplate on the changeTemplate event', async () => {
findEditMetaControls().vm.$emit('changeTemplate', template1);
await wrapper.vm.$nextTick();
expect(findEditMetaControls().props().currentTemplate).toBe(template1);
findEditMetaControls().vm.$emit('changeTemplate', null);
await wrapper.vm.$nextTick();
expect(findEditMetaControls().props().currentTemplate).toBe(null);
});
it('updates the description on the changeTemplate event', async () => {
findEditMetaControls().vm.$emit('changeTemplate', template1);
await wrapper.vm.$nextTick();
expect(findEditMetaControls().props().description).toEqual(template1.content);
});
});
it('emits the hide event', () => {
findGlModal().vm.$emit('hide');
expect(wrapper.emitted('hide')).toEqual([[]]);

View File

@ -48,6 +48,10 @@ export const savedContentMeta = {
url: 'foobar/-/merge_requests/123',
},
};
export const mergeRequestTemplates = [
{ key: 'Template1', name: 'Template 1', content: 'This is template 1!' },
{ key: 'Template2', name: 'Template 2', content: 'This is template 2!' },
];
export const submitChangesError = 'Could not save changes';
export const commitBranchResponse = {

View File

@ -517,6 +517,8 @@ module TestEnv
return false if component_matches_git_sha?(component_folder, expected_version)
return false if component_ahead_of_target?(component_folder, expected_version)
version = File.read(File.join(component_folder, 'VERSION')).strip
# Notice that this will always yield true when using branch versions
@ -527,6 +529,20 @@ module TestEnv
true
end
def component_ahead_of_target?(component_folder, expected_version)
# The HEAD of the component_folder will be used as heuristic for the version
# of the binaries, allowing to use Git to determine if HEAD is later than
# the expected version. Note: Git considers HEAD to be an anchestor of HEAD.
_out, exit_status = Gitlab::Popen.popen(%W[
#{Gitlab.config.git.bin_path}
-C #{component_folder}
merge-base --is-ancestor
#{expected_version} HEAD
])
exit_status == 0
end
def component_matches_git_sha?(component_folder, expected_version)
# Not a git SHA, so return early
return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID

View File

@ -89,4 +89,4 @@ DEPENDENCIES
scss_lint (~> 0.56.0)
BUNDLED WITH
1.17.3
2.1.4