Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-04 18:09:49 +00:00
parent aca89cb7e9
commit 2ecc6e22e3
89 changed files with 1888 additions and 294 deletions

View File

@ -1025,7 +1025,6 @@ Style/NumericLiteralPrefix:
Style/NumericPredicate:
EnforcedStyle: comparison
Exclude:
- 'spec/**/*'
- 'app/controllers/concerns/issuable_collections.rb'
- 'app/controllers/concerns/paginated_collection.rb'
- 'app/helpers/graph_helper.rb'
@ -1126,8 +1125,6 @@ Style/NumericPredicate:
- 'lib/tasks/gitlab/gitaly.rake'
- 'lib/tasks/gitlab/snippets.rake'
- 'lib/tasks/gitlab/workhorse.rake'
- 'qa/qa/git/repository.rb'
- 'qa/qa/support/wait_for_requests.rb'
- 'ee/app/models/ee/project.rb'
- 'lib/gitlab/usage_data/topology.rb'

View File

@ -1 +1 @@
d02d473234dc18649e5a359ee203d6a3aa5c4031
c6fdcae2d1c5d4914a010dfe7ea5dbfcfb8bdabf

View File

@ -0,0 +1,49 @@
<script>
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
},
props: {
commitsEmpty: {
type: Boolean,
required: false,
default: false,
},
contextCommitsEmpty: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.contextCommitsEmpty || this.commitsEmpty
? s__('AddContextCommits|Add previously merged commits')
: s__('AddContextCommits|Add/remove');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<gl-button
:class="[
{
'ml-3': !contextCommitsEmpty,
'mt-3': !commitsEmpty && contextCommitsEmpty,
},
]"
:variant="commitsEmpty ? 'info' : 'default'"
@click="openModal"
>
{{ buttonText }}
</gl-button>
</template>

View File

@ -0,0 +1,279 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import createFlash from '~/flash';
import {
findCommitIndex,
setCommitStatus,
removeIfReadyToBeRemoved,
removeIfPresent,
} from '../utils';
export default {
components: {
GlModal,
GlTabs,
GlTab,
ReviewTabContainer,
GlSearchBoxByType,
GlSprintf,
},
props: {
contextCommitsPath: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
mergeRequestIid: {
type: Number,
required: true,
},
projectId: {
type: Number,
required: true,
},
},
computed: {
...mapState([
'tabIndex',
'isLoadingCommits',
'commits',
'commitsLoadingError',
'isLoadingContextCommits',
'contextCommits',
'contextCommitsLoadingError',
'selectedCommits',
'searchText',
'toRemoveCommits',
]),
currentTabIndex: {
get() {
return this.tabIndex;
},
set(newTabIndex) {
this.setTabIndex(newTabIndex);
},
},
selectedCommitsCount() {
return this.selectedCommits.filter(selectedCommit => selectedCommit.isSelected).length;
},
shouldPurge() {
return this.selectedCommitsCount !== this.selectedCommits.length;
},
uniqueCommits() {
return this.selectedCommits.filter(
selectedCommit =>
selectedCommit.isSelected &&
findCommitIndex(this.contextCommits, selectedCommit.short_id) === -1,
);
},
disableSaveButton() {
// We should have a minimum of one commit selected and that should not be in the context commits list or we should have a context commit to delete
return (
(this.selectedCommitsCount.length === 0 || this.uniqueCommits.length === 0) &&
this.toRemoveCommits.length === 0
);
},
},
watch: {
tabIndex(newTabIndex) {
this.handleTabChange(newTabIndex);
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
this.setBaseConfig({
contextCommitsPath: this.contextCommitsPath,
mergeRequestIid: this.mergeRequestIid,
projectId: this.projectId,
});
},
beforeDestroy() {
eventHub.$off('openModal', this.openModal);
clearTimeout(this.timeout);
this.timeout = null;
},
methods: {
...mapActions([
'setBaseConfig',
'setTabIndex',
'searchCommits',
'setCommits',
'createContextCommits',
'fetchContextCommits',
'removeContextCommits',
'setSelectedCommits',
'setSearchText',
'setToRemoveCommits',
'resetModalState',
]),
focusSearch() {
this.$refs.searchInput.focusInput();
},
openModal() {
this.searchCommits();
this.fetchContextCommits();
this.$root.$emit('bv::show::modal', 'add-review-item');
},
handleTabChange(tabIndex) {
if (tabIndex === 0) {
this.focusSearch();
if (this.shouldPurge) {
this.setSelectedCommits(
[...this.commits, ...this.selectedCommits].filter(commit => commit.isSelected),
);
}
}
},
handleSearchCommits(value) {
// We only call the service, if we have 3 characters or we don't have any characters
if (value.length >= 3) {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.searchCommits(value);
}, 500);
} else if (value.length === 0) {
this.searchCommits();
}
this.setSearchText(value);
},
handleCommitRowSelect(event) {
const index = event[0];
const selected = event[1];
const tempCommit = this.tabIndex === 0 ? this.commits[index] : this.selectedCommits[index];
const commitIndex = findCommitIndex(this.commits, tempCommit.short_id);
const tempCommits = setCommitStatus(this.commits, commitIndex, selected);
const selectedCommitIndex = findCommitIndex(this.selectedCommits, tempCommit.short_id);
let tempSelectedCommits = setCommitStatus(
this.selectedCommits,
selectedCommitIndex,
selected,
);
if (selected) {
// If user deselects a commit which is already present in previously merged commits, then user adds it again.
// Then the state is neutral, so we remove it from the list
this.setToRemoveCommits(
removeIfReadyToBeRemoved(this.toRemoveCommits, tempCommit.short_id),
);
} else {
// If user is present in first tab and deselects a commit, remove it directly
if (this.tabIndex === 0) {
tempSelectedCommits = removeIfPresent(tempSelectedCommits, tempCommit.short_id);
}
// If user deselects a commit which is already present in previously merged commits, we keep track of it in a list to remove
const contextCommitsIndex = findCommitIndex(this.contextCommits, tempCommit.short_id);
if (contextCommitsIndex !== -1) {
this.setToRemoveCommits([...this.toRemoveCommits, tempCommit.short_id]);
}
}
this.setCommits({ commits: tempCommits });
this.setSelectedCommits([
...tempSelectedCommits,
...tempCommits.filter(commit => commit.isSelected),
]);
},
handleCreateContextCommits() {
if (this.uniqueCommits.length > 0 && this.toRemoveCommits.length > 0) {
return Promise.all([
this.createContextCommits({ commits: this.uniqueCommits }),
this.removeContextCommits(),
]).then(values => {
if (values[0] || values[1]) {
window.location.reload();
}
if (!values[0] && !values[1]) {
createFlash(
s__('ContextCommits|Failed to create/remove context commits. Please try again.'),
);
}
});
} else if (this.uniqueCommits.length > 0) {
return this.createContextCommits({ commits: this.uniqueCommits, forceReload: true });
}
return this.removeContextCommits(true);
},
handleModalClose() {
this.resetModalState();
clearTimeout(this.timeout);
},
handleModalHide() {
this.resetModalState();
clearTimeout(this.timeout);
},
},
};
</script>
<template>
<gl-modal
ref="modal"
cancel-variant="light"
size="md"
body-class="add-review-item pt-0"
:scrollable="true"
:ok-title="__('Save changes')"
modal-id="add-review-item"
:title="__('Add or remove previously merged commits')"
:ok-disabled="disableSaveButton"
@shown="focusSearch"
@ok="handleCreateContextCommits"
@cancel="handleModalClose"
@close="handleModalClose"
@hide="handleModalHide"
>
<gl-tabs v-model="currentTabIndex" content-class="pt-0">
<gl-tab>
<template #title>
<gl-sprintf :message="__(`Commits in %{codeStart}${targetBranch}%{codeEnd}`)">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</template>
<div class="mt-2">
<gl-search-box-by-type
ref="searchInput"
:placeholder="__(`Search by commit title or SHA`)"
@input="handleSearchCommits"
/>
<review-tab-container
:is-loading="isLoadingCommits"
:loading-error="commitsLoadingError"
:loading-failed-text="__('Unable to load commits. Try again later.')"
:commits="commits"
:empty-list-text="__('Your search didn\'t match any commits. Try a different query.')"
@handleCommitSelect="handleCommitRowSelect"
/>
</div>
</gl-tab>
<gl-tab>
<template #title>
{{ __('Selected commits') }}
<span class="badge badge-pill">{{ selectedCommitsCount }}</span>
</template>
<review-tab-container
:is-loading="isLoadingContextCommits"
:loading-error="contextCommitsLoadingError"
:loading-failed-text="__('Unable to load commits. Try again later.')"
:commits="selectedCommits"
:empty-list-text="
__(
'Commits you select appear here. Go to the first tab and select commits to add to this merge request.',
)
"
@handleCommitSelect="handleCommitRowSelect"
/>
</gl-tab>
</gl-tabs>
</gl-modal>
</template>

View File

@ -0,0 +1,57 @@
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import CommitItem from '~/diffs/components/commit_item.vue';
import { __ } from '~/locale';
export default {
components: {
GlLoadingIcon,
GlAlert,
CommitItem,
},
props: {
isLoading: {
type: Boolean,
required: true,
},
loadingError: {
type: Boolean,
required: true,
},
loadingFailedText: {
type: String,
required: true,
},
commits: {
type: Array,
required: true,
},
emptyListText: {
type: String,
required: false,
default: __('No commits present here'),
},
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
<gl-alert v-else-if="loadingError" variant="danger" :dismissible="false" class="mt-3">
{{ loadingFailedText }}
</gl-alert>
<div v-else-if="commits.length === 0" class="text-center mt-4">
<span>{{ emptyListText }}</span>
</div>
<div v-else>
<ul class="content-list commit-list flex-list">
<commit-item
v-for="(commit, index) in commits"
:key="commit.id"
:is-selectable="true"
:commit="commit"
:checked="commit.isSelected"
@handleCheckboxChange="$emit('handleCommitSelect', [index, $event])"
/>
</ul>
</div>
</template>

View File

@ -0,0 +1,3 @@
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();

View File

@ -0,0 +1,64 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import createStore from './store';
import AddContextCommitsModalTrigger from './components/add_context_commits_modal_trigger.vue';
import AddContextCommitsModalWrapper from './components/add_context_commits_modal_wrapper.vue';
export default function initAddContextCommitsTriggers() {
const addContextCommitsModalTriggerEl = document.querySelector('.add-review-item-modal-trigger');
const addContextCommitsModalWrapperEl = document.querySelector('.add-review-item-modal-wrapper');
if (addContextCommitsModalTriggerEl || addContextCommitsModalWrapperEl) {
// eslint-disable-next-line no-new
new Vue({
el: addContextCommitsModalTriggerEl,
data() {
const { commitsEmpty, contextCommitsEmpty } = this.$options.el.dataset;
return {
commitsEmpty: parseBoolean(commitsEmpty),
contextCommitsEmpty: parseBoolean(contextCommitsEmpty),
};
},
render(createElement) {
return createElement(AddContextCommitsModalTrigger, {
props: {
commitsEmpty: this.commitsEmpty,
contextCommitsEmpty: this.contextCommitsEmpty,
},
});
},
});
const store = createStore();
// eslint-disable-next-line no-new
new Vue({
el: addContextCommitsModalWrapperEl,
store,
data() {
const {
contextCommitsPath,
targetBranch,
mergeRequestIid,
projectId,
} = this.$options.el.dataset;
return {
contextCommitsPath,
targetBranch,
mergeRequestIid: Number(mergeRequestIid),
projectId: Number(projectId),
};
},
render(createElement) {
return createElement(AddContextCommitsModalWrapper, {
props: {
contextCommitsPath: this.contextCommitsPath,
targetBranch: this.targetBranch,
mergeRequestIid: this.mergeRequestIid,
projectId: this.projectId,
},
});
},
});
}
}

View File

@ -0,0 +1,134 @@
import _ from 'lodash';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types';
export const setBaseConfig = ({ commit }, options) => {
commit(types.SET_BASE_CONFIG, options);
};
export const setTabIndex = ({ commit }, tabIndex) => commit(types.SET_TABINDEX, tabIndex);
export const searchCommits = ({ dispatch, commit, state }, searchText) => {
commit(types.FETCH_COMMITS);
let params = {};
if (searchText) {
params = {
params: {
search: searchText,
per_page: 40,
},
};
}
return axios
.get(state.contextCommitsPath, params)
.then(({ data }) => {
let commits = data.map(o => ({ ...o, isSelected: false }));
commits = commits.map(c => {
const isPresent = state.selectedCommits.find(
selectedCommit => selectedCommit.short_id === c.short_id && selectedCommit.isSelected,
);
if (isPresent) {
return { ...c, isSelected: true };
}
return c;
});
if (!searchText) {
dispatch('setCommits', { commits: [...commits, ...state.contextCommits] });
} else {
dispatch('setCommits', { commits });
}
})
.catch(() => {
commit(types.FETCH_COMMITS_ERROR);
});
};
export const setCommits = ({ commit }, { commits: data, silentAddition = false }) => {
let commits = _.uniqBy(data, 'short_id');
commits = _.orderBy(data, c => new Date(c.committed_date), ['desc']);
if (silentAddition) {
commit(types.SET_COMMITS_SILENT, commits);
} else {
commit(types.SET_COMMITS, commits);
}
};
export const createContextCommits = ({ state }, { commits, forceReload = false }) =>
Api.createContextCommits(state.projectId, state.mergeRequestIid, {
commits: commits.map(commit => commit.short_id),
})
.then(() => {
if (forceReload) {
window.location.reload();
}
return true;
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to create context commits. Please try again.'));
}
return false;
});
export const fetchContextCommits = ({ dispatch, commit, state }) => {
commit(types.FETCH_CONTEXT_COMMITS);
return Api.allContextCommits(state.projectId, state.mergeRequestIid)
.then(({ data }) => {
const contextCommits = data.map(o => ({ ...o, isSelected: true }));
dispatch('setContextCommits', contextCommits);
dispatch('setCommits', {
commits: [...state.commits, ...contextCommits],
silentAddition: true,
});
dispatch('setSelectedCommits', contextCommits);
})
.catch(() => {
commit(types.FETCH_CONTEXT_COMMITS_ERROR);
});
};
export const setContextCommits = ({ commit }, data) => {
commit(types.SET_CONTEXT_COMMITS, data);
};
export const removeContextCommits = ({ state }, forceReload = false) =>
Api.removeContextCommits(state.projectId, state.mergeRequestIid, {
commits: state.toRemoveCommits,
})
.then(() => {
if (forceReload) {
window.location.reload();
}
return true;
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to delete context commits. Please try again.'));
}
return false;
});
export const setSelectedCommits = ({ commit }, selected) => {
let selectedCommits = _.uniqBy(selected, 'short_id');
selectedCommits = _.orderBy(
selectedCommits,
selectedCommit => new Date(selectedCommit.committed_date),
['desc'],
);
commit(types.SET_SELECTED_COMMITS, selectedCommits);
};
export const setSearchText = ({ commit }, searchText) => commit(types.SET_SEARCH_TEXT, searchText);
export const setToRemoveCommits = ({ commit }, data) => commit(types.SET_TO_REMOVE_COMMITS, data);
export const resetModalState = ({ commit }) => commit(types.RESET_MODAL_STATE);

View File

@ -0,0 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
namespaced: true,
state: state(),
actions,
mutations,
});

View File

@ -0,0 +1,20 @@
export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
export const SET_TABINDEX = 'SET_TABINDEX';
export const FETCH_COMMITS = 'FETCH_COMMITS';
export const SET_COMMITS = 'SET_COMMITS';
export const SET_COMMITS_SILENT = 'SET_COMMITS_SILENT';
export const FETCH_COMMITS_ERROR = 'FETCH_COMMITS_ERROR';
export const FETCH_CONTEXT_COMMITS = 'FETCH_CONTEXT_COMMITS';
export const SET_CONTEXT_COMMITS = 'SET_CONTEXT_COMMITS';
export const FETCH_CONTEXT_COMMITS_ERROR = 'FETCH_CONTEXT_COMMITS_ERROR';
export const SET_SELECTED_COMMITS = 'SET_SELECTED_COMMITS';
export const SET_SEARCH_TEXT = 'SET_SEARCH_TEXT';
export const SET_TO_REMOVE_COMMITS = 'SET_TO_REMOVE_COMMITS';
export const RESET_MODAL_STATE = 'RESET_MODAL_STATE';

View File

@ -0,0 +1,56 @@
import * as types from './mutation_types';
export default {
[types.SET_BASE_CONFIG](state, options) {
Object.assign(state, { ...options });
},
[types.SET_TABINDEX](state, tabIndex) {
state.tabIndex = tabIndex;
},
[types.FETCH_COMMITS](state) {
state.isLoadingCommits = true;
state.commitsLoadingError = false;
},
[types.SET_COMMITS](state, commits) {
state.commits = commits;
state.isLoadingCommits = false;
state.commitsLoadingError = false;
},
[types.SET_COMMITS_SILENT](state, commits) {
state.commits = commits;
},
[types.FETCH_COMMITS_ERROR](state) {
state.commitsLoadingError = true;
state.isLoadingCommits = false;
},
[types.FETCH_CONTEXT_COMMITS](state) {
state.isLoadingContextCommits = true;
state.contextCommitsLoadingError = false;
},
[types.SET_CONTEXT_COMMITS](state, contextCommits) {
state.contextCommits = contextCommits;
state.isLoadingContextCommits = false;
state.contextCommitsLoadingError = false;
},
[types.FETCH_CONTEXT_COMMITS_ERROR](state) {
state.contextCommitsLoadingError = true;
state.isLoadingContextCommits = false;
},
[types.SET_SELECTED_COMMITS](state, commits) {
state.selectedCommits = commits;
},
[types.SET_SEARCH_TEXT](state, searchText) {
state.searchText = searchText;
},
[types.SET_TO_REMOVE_COMMITS](state, commits) {
state.toRemoveCommits = commits;
},
[types.RESET_MODAL_STATE](state) {
state.tabIndex = 0;
state.commits = [];
state.contextCommits = [];
state.selectedCommits = [];
state.toRemoveCommits = [];
state.searchText = '';
},
};

View File

@ -0,0 +1,13 @@
export default () => ({
contextCommitsPath: '',
tabIndex: 0,
isLoadingCommits: false,
commits: [],
commitsLoadingError: false,
selectedCommits: [],
isLoadingContextCommits: false,
contextCommits: [],
contextCommitsLoadingError: false,
searchText: '',
toRemoveCommits: [],
});

View File

@ -0,0 +1,32 @@
export const findCommitIndex = (commits, commitShortId) => {
return commits.findIndex(commit => commit.short_id === commitShortId);
};
export const setCommitStatus = (commits, commitIndex, selected) => {
const tempCommits = [...commits];
tempCommits[commitIndex] = {
...tempCommits[commitIndex],
isSelected: selected,
};
return tempCommits;
};
export const removeIfReadyToBeRemoved = (toRemoveCommits, commitShortId) => {
const tempToRemoveCommits = [...toRemoveCommits];
const isPresentInToRemove = tempToRemoveCommits.indexOf(commitShortId);
if (isPresentInToRemove !== -1) {
tempToRemoveCommits.splice(isPresentInToRemove, 1);
}
return tempToRemoveCommits;
};
export const removeIfPresent = (selectedCommits, commitShortId) => {
const tempSelectedCommits = [...selectedCommits];
const selectedCommitsIndex = findCommitIndex(tempSelectedCommits, commitShortId);
if (selectedCommitsIndex !== -1) {
tempSelectedCommits.splice(selectedCommitsIndex, 1);
}
return tempSelectedCommits;
};

View File

@ -57,6 +57,8 @@ const Api = {
pipelinesPath: '/api/:version/projects/:id/pipelines/',
createPipelinePath: '/api/:version/projects/:id/pipeline',
environmentsPath: '/api/:version/projects/:id/environments',
contextCommitsPath:
'/api/:version/projects/:id/merge_requests/:merge_request_iid/context_commits',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
@ -598,6 +600,30 @@ const Api = {
return axios.get(url);
},
createContextCommits(id, mergeRequestIid, data) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', encodeURIComponent(id))
.replace(':merge_request_iid', mergeRequestIid);
return axios.post(url, data);
},
allContextCommits(id, mergeRequestIid) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', encodeURIComponent(id))
.replace(':merge_request_iid', mergeRequestIid);
return axios.get(url);
},
removeContextCommits(id, mergeRequestIid, data) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', id)
.replace(':merge_request_iid', mergeRequestIid);
return axios.delete(url, { data });
},
getRawFile(id, path, params = { ref: 'master' }) {
const url = Api.buildUrl(this.rawFilePath)
.replace(':id', encodeURIComponent(id))

View File

@ -52,10 +52,20 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
isSelectable: {
type: Boolean,
required: false,
default: false,
},
commit: {
type: Object,
required: true,
},
checked: {
type: Boolean,
required: false,
default: false,
},
collapsible: {
type: Boolean,
required: false,
@ -83,6 +93,10 @@ export default {
authorAvatar() {
return this.author.avatar_url || this.commit.author_gravatar_url;
},
commitDescription() {
// Strip the newline at the beginning
return this.commit.description_html.replace(/^&#x000A;/, '');
},
nextCommitUrl() {
return this.commit.next_commit_id
? setUrlParams({ commit_id: this.commit.next_commit_id })
@ -110,13 +124,22 @@ export default {
<template>
<li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row">
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
<div class="d-flex align-items-center align-self-start">
<input
v-if="isSelectable"
class="mr-2"
type="checkbox"
:checked="checked"
@change="$emit('handleCheckboxChange', $event.target.checked)"
/>
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
</div>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
@ -151,7 +174,7 @@ export default {
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 text-dark"
v-html="commit.description_html"
v-html="commitDescription"
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">

View File

@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
import initAddContextCommitsTriggers from './add_context_commits_modal';
import { __ } from './locale';
// MergeRequestTabs
@ -340,6 +341,7 @@ export default class MergeRequestTabs {
this.scrollToElement('#commits');
this.toggleLoading(false);
initAddContextCommitsTriggers();
})
.catch(() => {
this.toggleLoading(false);

View File

@ -7,6 +7,7 @@ export default () => ({
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
dashboardsEndpoint: invalidUrl,
panelPreviewEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,

View File

@ -24,6 +24,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,
@ -45,6 +46,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,

View File

@ -1,5 +1,5 @@
<script>
import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
@ -9,8 +9,7 @@ export default {
},
components: {
CiStatus,
GlLoadingIcon,
GlDeprecatedButton,
GlButton,
},
props: {
pipeline: {
@ -95,26 +94,21 @@ export default {
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
<gl-deprecated-button
<gl-button
:id="buttonId"
v-gl-tooltip
:title="tooltipText"
class="js-linked-pipeline-content linked-pipeline-content"
class="linked-pipeline-content"
data-qa-selector="linked_pipeline_button"
:class="`js-pipeline-expand-${pipeline.id}`"
:loading="pipeline.isLoading"
@click="onClickLinkedPipeline"
>
<gl-loading-icon v-if="pipeline.isLoading" class="js-linked-pipeline-loading d-inline" />
<ci-status
v-else
:status="pipelineStatus"
css-classes="position-top-0"
class="js-linked-pipeline-status"
/>
<ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" />
<span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
<div class="gl-pt-2">
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
</div>
</gl-deprecated-button>
</gl-button>
</li>
</template>

View File

@ -388,3 +388,9 @@
display: block;
color: $link-color;
}
.add-review-item {
.gl-tab-nav-item {
height: 100%;
}
}

View File

@ -225,7 +225,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:lets_encrypt_terms_of_service_accepted,
:domain_blacklist_file,
:raw_blob_request_limit,
:namespace_storage_size_limit,
:issues_create_limit,
:default_branch_name,
disabled_oauth_sign_in_sources: [],

View File

@ -82,7 +82,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@note = @project.notes.new(noteable: @merge_request)
@noteable = @merge_request
@commits_count = @merge_request.commits_count
@commits_count = @merge_request.commits_count + @merge_request.context_commits_count
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
@current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
@ -116,6 +116,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def commits
# Get context commits from repository
@context_commits =
set_commits_for_rendering(
@merge_request.recent_context_commits
)
# Get commits from repository
# or from cache if already merged
@commits =

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Projects
module Metrics
module Dashboards
class BuilderController < Projects::ApplicationController
before_action :ensure_feature_flags
before_action :authorize_metrics_dashboard!
def panel_preview
respond_to do |format|
format.json { render json: render_panel }
end
end
private
def ensure_feature_flags
render_404 unless Feature.enabled?(:metrics_dashboard_new_panel_page, project)
end
def render_panel
{
"title": "Memory Usage (Total)",
"type": "area-chart",
"y_label": "Total Memory Used (GB)",
"weight": 4,
"metrics": [
{
"id": "system_metrics_kubernetes_container_memory_total",
"query_range": "avg(sum(container_memory_usage_bytes{container_name!=\"POD\",pod_name=~\"^{{ci_environment_slug}}-(.*)\",namespace=\"{{kube_namespace}}\"}) by (job)) without (job) /1024/1024/1024",
"label": "Total (GB)",
"unit": "GB",
"metric_id": 15,
"edit_path": nil,
"prometheus_endpoint_path": "/root/autodevops-deploy/-/environments/29/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%7B%7Bci_environment_slug%7D%7D-%28.%2A%29%22%2Cnamespace%3D%22%7B%7Bkube_namespace%7D%7D%22%7D%29+by+%28job%29%29+without+%28job%29++%2F1024%2F1024%2F1024"
}
],
"id": "4570deed516d0bf93fb42879004117009ab456ced27393ec8dce5b6960438132"
}
end
end
end
end
end

View File

@ -25,7 +25,7 @@ class ContextCommitsFinder
if search.present?
search_commits
else
project.repository.commits(merge_request.source_branch, { limit: limit, offset: offset })
project.repository.commits(merge_request.target_branch, { limit: limit, offset: offset })
end
commits
@ -47,7 +47,7 @@ class ContextCommitsFinder
commits = [commit_by_sha] if commit_by_sha
end
else
commits = project.repository.find_commits_by_message(search, nil, nil, 20)
commits = project.repository.find_commits_by_message(search, merge_request.target_branch, nil, 20)
end
commits

View File

@ -98,7 +98,8 @@ module EnvironmentsHelper
'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json),
'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
'operations-settings-path' => project_settings_operations_path(project),
'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s
'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s,
'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
}
end

View File

@ -5,6 +5,9 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
include IgnorableColumns
ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
@ -363,10 +366,6 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 255 },
allow_blank: true
validates :namespace_storage_size_limit,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :issues_create_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }

View File

@ -96,7 +96,6 @@ module ApplicationSettingImplementation
max_import_size: 50,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
namespace_storage_size_limit: 0,
notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,

View File

@ -40,7 +40,7 @@ class MergeRequest < ApplicationRecord
has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs
has_many :merge_request_context_commits
has_many :merge_request_context_commits, inverse_of: :merge_request
has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files
has_one :merge_request_diff,
@ -427,7 +427,7 @@ class MergeRequest < ApplicationRecord
end
def context_commits(limit: nil)
@context_commits ||= merge_request_context_commits.limit(limit).map(&:to_commit)
@context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit)
end
def recent_context_commits

View File

@ -12,6 +12,9 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
# Sort by committed date in descending order to ensure latest commits comes on the top
scope :order_by_committed_date_desc, -> { order('committed_date DESC') }
# delete all MergeRequestContextCommit & MergeRequestContextCommitDiffFile for given merge_request & commit SHAs
def self.delete_bulk(merge_request, commits)
commit_ids = commits.map(&:sha)

View File

@ -8,14 +8,6 @@
= f.label :gravatar_enabled, class: 'form-check-label' do
= _('Gravatar enabled')
.form-group
= f.label :namespace_storage_size_limit, class: 'label-bold' do
= _('Maximum namespace storage (MB)')
= f.number_field :namespace_storage_size_limit, class: 'form-control', min: 0
%span.form-text.text-muted
= _('Includes repository storage, wiki storage, LFS objects, build artifacts and packages. 0 for unlimited.')
= link_to _('More information'), help_page_path('user/admin_area/settings/account_and_limit_settings', anchor: 'maximum-namespace-storage-size'), target: '_blank'
.form-group
= f.label :default_projects_limit, _('Default projects limit'), class: 'label-bold'
= f.number_field :default_projects_limit, class: 'form-control', title: _('Maximum number of projects.'), data: { toggle: 'tooltip', container: 'body' }

View File

@ -1,8 +1,10 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- commits = @commits
- context_commits = @context_commits
- hidden = @hidden_commit_count
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
@ -14,11 +16,26 @@
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if context_commits.present?
%li.commit-header.js-commit-header
%span.font-weight-bold= n_("%d previously merged commit", "%d previously merged commits", context_commits.count) % context_commits.count
- if project.context_commits_enabled? && can_update_merge_request
%button.btn.btn-default.ml-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'false' } }
= _('Add/remove')
%li.commits-row
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.alert.alert-warning
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if commits.size == 0
- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
%button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } }
= _('Add previously merged commits')
- if commits.size == 0 && context_commits.nil?
.mt-4.text-center
.bold
= _('Your search didn\'t match any commits.')

View File

@ -1,8 +1,18 @@
- if @commits.empty?
.commits-empty
%h4
There are no commits yet.
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- if @commits.empty? && @context_commits.empty?
.commits-empty.mt-5
= custom_icon ('illustration_no_commits')
%h4
= _('There are no commits yet.')
- if @project&.context_commits_enabled? && can_update_merge_request
%p
= _('Push commits to the source branch or add previously merged commits to review them.')
%button.btn.btn-primary.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
= _('Add previously merged commits')
- else
%ol#commits-list.list-unstyled
= render "projects/commits/commits", merge_request: @merge_request
- if @project&.context_commits_enabled? && can_update_merge_request && @merge_request.iid
.add-review-item-modal-wrapper{ data: { context_commits_path: context_commits_project_json_merge_request_url(@merge_request&.project, @merge_request, :json), target_branch: @merge_request.target_branch, merge_request_iid: @merge_request.iid, project_id: @merge_request.project.id } }

View File

@ -0,0 +1,5 @@
---
title: Improve performance of Banzai reference filters
merge_request: 38290
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Replace <gl-deprecated-button> with <gl-button> in app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
merge_request: 36968
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove namespace storage limit setting
merge_request: 38108
author:
type: removed

View File

@ -28,6 +28,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'metrics(/:dashboard_path)(/:page)', constraints: { dashboard_path: /.+\.yml/, page: 'panel/new' },
to: 'metrics_dashboard#show', as: :metrics_dashboard, format: false
namespace :metrics, module: :metrics do
namespace :dashboards do
post :builder, to: 'builder#panel_preview'
end
end
resources :artifacts, only: [:index, :destroy]
resources :packages, only: [:index, :show, :destroy], module: :packages

View File

@ -141,7 +141,7 @@ sudo gitlab-rake geo:verification:wiki:reset
If the **primary** and **secondary** nodes have a checksum verification mismatch, the cause may not be apparent. To find the cause of a checksum mismatch:
1. Navigate to the **Admin Area >** **{overview}** **Overview > Projects** dashboard on the **primary** node, find the
1. Navigate to the **Admin Area > Overview > Projects** dashboard on the **primary** node, find the
project that you want to check the checksum differences and click on the
**Edit** button:
![Projects dashboard](img/checksum-differences-admin-projects.png)

View File

@ -135,7 +135,7 @@ This [content was moved to another location](background_verification.md).
### Notify users of scheduled maintenance
On the **primary** node, navigate to **Admin Area >** **{bullhorn}** **Messages**, add a broadcast
On the **primary** node, navigate to **Admin Area > Messages**, add a broadcast
message. You can check under **Admin Area > Geo** to estimate how long it
will take to finish syncing. An example message would be:
@ -181,7 +181,7 @@ access to the **primary** node during the maintenance window.
connection.
1. Disable non-Geo periodic background jobs on the **primary** node by navigating
to **Admin Area >** **{monitor}** **Monitoring > Background Jobs > Cron**, pressing `Disable All`,
to **Admin Area > Monitoring > Background Jobs > Cron**, pressing `Disable All`,
and then pressing `Enable` for the `geo_sidekiq_cron_config_worker` cron job.
This job will re-enable several other cron jobs that are essential for planned
failover to complete successfully.
@ -190,7 +190,7 @@ access to the **primary** node during the maintenance window.
1. If you are manually replicating any data not managed by Geo, trigger the
final replication process now.
1. On the **primary** node, navigate to **Admin Area >** **{monitor}** **Monitoring > Background Jobs > Queues**
1. On the **primary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
and wait for all queues except those with `geo` in the name to drop to 0.
These queues contain work that has been submitted by your users; failing over
before it is completed will cause the work to be lost.
@ -202,7 +202,7 @@ access to the **primary** node during the maintenance window.
- Database replication lag is 0ms.
- The Geo log cursor is up to date (0 events behind).
1. On the **secondary** node, navigate to **Admin Area >** **{monitor}** **Monitoring > Background Jobs > Queues**
1. On the **secondary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
and wait for all the `geo` queues to drop to 0 queued and 0 running jobs.
1. On the **secondary** node, use [these instructions](../../raketasks/check.md)
to verify the integrity of CI artifacts, LFS objects, and uploads in file

View File

@ -10,7 +10,7 @@ To profile a request:
1. Sign in to GitLab as a user with Administrator or Maintainer [permissions](../../../user/permissions.md).
1. In the navigation bar, click **Admin area**.
1. Navigate to **{monitor}** **Monitoring > Requests Profiles**.
1. Navigate to **Monitoring > Requests Profiles**.
1. In the **Requests Profiles** section, copy the token.
1. Pass the headers `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where
`<mode>` can be `execution` or `memory`) to the request you want to profile. When
@ -29,7 +29,7 @@ To profile a request:
Profiled requests can take longer than usual.
After the request completes, you can view the profiling output from the
**{monitor}** **Monitoring > Requests Profiles** administration page:
**Monitoring > Requests Profiles** administration page:
![Profiling output](img/request_profile_result.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -15,10 +15,10 @@ of incident management are only available in
[GitLab Ultimate and GitLab.com Gold](https://about.gitlab.com/pricing/).
For users with at least Developer [permissions](../../user/permissions.md), the
Incident Management list is available at **{cloud-gear}** **Operations > Incidents**
Incident Management list is available at **Operations > Incidents**
in your project's sidebar. The list contains the following metrics:
![Incident Management List](img/incident_list_13_3.png)
![Incident Management List](img/incident_list_v13_3.png)
- **Incident** - The description of the incident, which attempts to capture the
most meaningful data.
@ -34,9 +34,9 @@ Incidents share the [Issues API](../../user/project/issues/index.md).
> [Moved](https://gitlab.com/gitlab-org/monitor/health/-/issues/24) to GitLab core in 13.3.
To create a Incident you can take any of the following actions:
For users with at least Developer [permissions](../../user/permissions.md), to create a Incident you can take any of the following actions:
- Navigate to **{cloud-gear}** **Operations > Incidents** and click **Create Incident**.
- Navigate to **Operations > Incidents** and click **Create Incident**.
- Create a new issue using the `incident` template available when creating it.
- Create a new issue and assign the `incident` label to it.
@ -44,7 +44,7 @@ To create a Incident you can take any of the following actions:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4925) in GitLab Ultimate 11.11.
You can enable or disable Incident Management features in the GitLab user interface
With Maintainer or higher [permissions](../../user/permissions.md), you can enable or disable Incident Management features in the GitLab user interface
to create issues when alerts are triggered:
1. Navigate to **Settings > Operations > Incidents** and expand

View File

@ -19,7 +19,7 @@ For managed Prometheus instances using auto configuration, you can
[configure alerts for metrics](index.md#adding-custom-metrics) directly in the
[metrics dashboard](index.md). To set an alert:
1. In your project, navigate to **{cloud-gear}** **Operations > Metrics**,
1. In your project, navigate to **Operations > Metrics**,
1. Identify the metric you want to create the alert for, and click the
**ellipsis** **{ellipsis_v}** icon in the top right corner of the metric.
1. Choose **Alerts**.

View File

@ -23,15 +23,14 @@ The metrics as defined below do not support alerts, unlike
> UI option [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223204) in GitLab 13.2.
You can configure a custom dashboard by adding a new YAML file into your project's
`.gitlab/dashboards/` directory. For the dashboard to display on your project's
**{cloud-gear}** **Operations > Metrics** page, the files must have a `.yml`
`.gitlab/dashboards/` directory. For the dashboard to display on your project's **Operations > Metrics** page, the files must have a `.yml`
extension and be present in your project's **default** branch.
To create a new dashboard from the GitLab user interface:
1. Sign in to GitLab as a user with Maintainer or Owner
[permissions](../../../user/permissions.md#project-members-permissions).
1. Navigate to your dashboard at **{cloud-gear}** **Operations > Metrics**.
1. Navigate to your dashboard at **Operations > Metrics**.
1. In the top-right corner of your dashboard, click the **{file-addition-solid}** **Actions** menu,
and select **Create new**:
![Monitoring Dashboard actions menu with create new item](img/actions_menu_create_new_dashboard_v13_2.png)
@ -103,7 +102,7 @@ To manage the settings for your metrics dashboard:
1. Sign in as a user with project Maintainer or Admin
[permissions](../../../user/permissions.md#project-members-permissions).
1. Navigate to your dashboard at **{cloud-gear}** **Operations > Metrics**.
1. Navigate to your dashboard at **Operations > Metrics**.
1. In the top-right corner of your dashboard, click **Metrics Settings**:
![Monitoring Dashboard actions menu with create new item](img/metrics_settings_button_v13_2.png)

View File

@ -20,7 +20,7 @@ To view the metrics dashboard for an environment that has
1. *If the metrics dashboard is only visible to project members,* sign in to
GitLab as a member of a project. Learn more about [metrics dashboard visibility](#metrics-dashboard-visibility).
1. In your project, navigate to **{cloud-gear}** **Operations > Metrics**.
1. In your project, navigate to **Operations > Metrics**.
GitLab displays the default metrics dashboard for the environment, like the
following example:
@ -52,11 +52,11 @@ navigation bar contains:
## Populate your metrics dashboard
After [configuring Prometheus for a cluster](../../user/project/integrations/prometheus.md),
you must also deploy code for the **{cloud-gear}** **Operations > Metrics** page
you must also deploy code for the **Operations > Metrics** page
to contain data. Setting up [Auto DevOps](../../topics/autodevops/index.md)
helps quickly create a deployment:
1. Navigate to your project's **{cloud-gear}** **Operations > Kubernetes** page.
1. Navigate to your project's **Operations > Kubernetes** page.
1. Ensure that, in addition to Prometheus, you also have Runner and Ingress
installed.
1. After installing Ingress, copy its endpoint.
@ -68,7 +68,7 @@ helps quickly create a deployment:
1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** page, and run a
pipeline on any branch.
1. When the pipeline has run successfully, graphs are available on the
**{cloud-gear}** **Operations > Metrics** page.
**Operations > Metrics** page.
![Monitoring Dashboard](img/prometheus_monitoring_dashboard_v13_1.png)

View File

@ -52,7 +52,7 @@ user interface:
1. Sign in to GitLab as a user with Reporter or greater
[permissions](../user/permissions.md).
1. Navigate to **{cloud-gear}** **Operations > Product Analytics**
1. Navigate to **Operations > Product Analytics**
The user interface contains:

View File

@ -276,14 +276,14 @@ The following table is an example of how to configure the three different cluste
To add a different cluster for each environment:
1. Navigate to your project's **{cloud-gear}** **Operations > Kubernetes**.
1. Navigate to your project's **Operations > Kubernetes**.
1. Create the Kubernetes clusters with their respective environment scope, as
described from the table above.
1. After creating the clusters, navigate to each cluster and install
Ingress. Wait for the Ingress IP address to be assigned.
1. Make sure you've [configured your DNS](#auto-devops-base-domain) with the
specified Auto DevOps domains.
1. Navigate to each cluster's page, through **{cloud-gear}** **Operations > Kubernetes**,
1. Navigate to each cluster's page, through **Operations > Kubernetes**,
and add the domain based on its Ingress IP address.
After completing configuration, you can test your setup by creating a merge request

View File

@ -57,7 +57,7 @@ to deploy this project to.
## Create a Kubernetes cluster from within GitLab
1. On your project's landing page, click **Add Kubernetes cluster**
(note that this option is also available when you navigate to **{cloud-gear}** **Operations > Kubernetes**).
(note that this option is also available when you navigate to **Operations > Kubernetes**).
![Project landing page](img/guide_project_landing_page_v12_10.png)
@ -194,7 +194,7 @@ to monitor it.
After successfully deploying your application, you can view its website and check
on its health on the **Environments** page by navigating to
**{cloud-gear}** **Operations > Environments**. This page displays details about
**Operations > Environments**. This page displays details about
the deployed applications, and the right-hand column displays icons that link
you to common environment tasks:

View File

@ -650,6 +650,6 @@ To use Auto Monitoring:
1. After the pipeline finishes successfully, open the
[monitoring dashboard for a deployed environment](../../ci/environments/index.md#monitoring-environments)
to view the metrics of your deployed application. To view the metrics of the
whole Kubernetes cluster, navigate to **{cloud-gear}** **Operations > Metrics**.
whole Kubernetes cluster, navigate to **Operations > Metrics**.
![Auto Metrics](img/auto_monitoring.png)

View File

@ -59,13 +59,13 @@ The Dashboard is the default view of the Admin Area, and is made up of the follo
## Overview section
The following topics document the **{overview}** **Overview** section of the Admin Area.
The following topics document the **Overview** section of the Admin Area.
### Administering Projects
You can administer all projects in the GitLab instance from the Admin Area's Projects page.
To access the Projects page, go to **Admin Area >** **{overview}** **Overview > Projects**.
To access the Projects page, go to **Admin Area > Overview > Projects**.
Click the **All**, **Private**, **Internal**, or **Public** tab to list only projects of that
criteria.
@ -105,7 +105,7 @@ You can combine the filter options. For example, to list only public projects wi
You can administer all users in the GitLab instance from the Admin Area's Users page.
To access the Users page, go to **Admin Area >** **{overview}** **Overview > Users**.
To access the Users page, go to **Admin Area > Overview > Users**.
To list users matching a specific criteria, click on one of the following tabs on the **Users** page:
@ -157,7 +157,7 @@ reflected in the statistics.
You can administer all groups in the GitLab instance from the Admin Area's Groups page.
To access the Groups page, go to **Admin Area >** **{overview}** **Overview > Groups**.
To access the Groups page, go to **Admin Area > Overview > Groups**.
For each group, the page displays their name, description, size, number of projects in the group,
number of members, and whether the group is private, internal, or public. To edit a group, click
@ -176,7 +176,7 @@ To [Create a new group](../group/index.md#create-a-new-group) click **New group*
You can administer all jobs in the GitLab instance from the Admin Area's Jobs page.
To access the Jobs page, go to **Admin Area >** **{overview}** **Overview > Jobs**.
To access the Jobs page, go to **Admin Area > Overview > Jobs**.
All jobs are listed, in descending order of job ID.
@ -201,7 +201,7 @@ For each job, the following details are listed:
You can administer all Runners in the GitLab instance from the Admin Area's **Runners** page. See
[GitLab Runner](https://docs.gitlab.com/runner/) for more information on Runner itself.
To access the **Runners** page, go to **Admin Area >** **{overview}** **Overview > Runners**.
To access the **Runners** page, go to **Admin Area > Overview > Runners**.
The **Runners** page features:
@ -247,7 +247,7 @@ You can also edit, pause, or remove each Runner.
You can list all Gitaly servers in the GitLab instance from the Admin Area's **Gitaly Servers**
page. For more details, see [Gitaly](../../administration/gitaly/index.md).
To access the **Gitaly Servers** page, go to **Admin Area >** **{overview}** **Overview > Gitaly Servers**.
To access the **Gitaly Servers** page, go to **Admin Area > Overview > Gitaly Servers**.
For each Gitaly server, the following details are listed:
@ -261,7 +261,7 @@ For each Gitaly server, the following details are listed:
## Monitoring section
The following topics document the **{monitor}** **Monitoring** section of the Admin Area.
The following topics document the **Monitoring** section of the Admin Area.
### System Info

View File

@ -29,20 +29,6 @@ If you choose a size larger than what is currently configured for the web server
you will likely get errors. See the [troubleshooting section](#troubleshooting) for more
details.
## Maximum namespace storage size
This sets a maximum size limit on each namespace. The following are included in the namespace size:
- Repository
- Wiki
- LFS objects
- Build artifacts
- Packages
- Snippets
NOTE: **Note:**
This limit is not currently enforced but will be in a future release.
## Repository size limit **(STARTER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/releases/2016/09/22/gitlab-8-12-released/#limit-project-size-ee).

View File

@ -507,6 +507,7 @@ To use SAST in an offline environment, you need:
- To keep Docker-In-Docker disabled (default).
- A GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
- A Docker Container Registry with locally available copies of SAST [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
- Configure certificate checking of packages (optional).
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
@ -563,6 +564,13 @@ variables:
The SAST job should now use local copies of the SAST analyzers to scan your code and generate
security reports without requiring internet access.
### Configure certificate checking of packages
If a SAST job invokes a package manager, you must configure its certificate verification. In an
offline environment, certificate verification with an external source isn't possible. Either use a
self-signed certificate or disable certificate verification. Refer to the package manager's
documentation for instructions.
## Troubleshooting
### `Error response from daemon: error processing tar file: docker-tar: relocation error`

View File

@ -28,9 +28,9 @@ This namespace:
To see a list of available applications to install. For a:
- [Project-level cluster](../project/clusters/index.md), navigate to your project's
**{cloud-gear}** **Operations > Kubernetes**.
**Operations > Kubernetes**.
- [Group-level cluster](../group/clusters/index.md), navigate to your group's
**{cloud-gear}** **Kubernetes** page.
**Kubernetes** page.
NOTE: **Note:**
As of GitLab 11.6, Helm will be upgraded to the latest version supported
@ -343,7 +343,7 @@ To help you tune your WAF rules, you can globally set your WAF to either
To change your WAF's mode:
1. [Install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md) if you have not already done so.
1. Navigate to **{cloud-gear}** **Operations > Kubernetes**.
1. Navigate to **Operations > Kubernetes**.
1. In **Applications**, scroll to **Ingress**.
1. Under **Global default**, select your desired mode.
1. Click **Save changes**.
@ -535,7 +535,7 @@ To enable log shipping:
1. Ensure your cluster contains at least 3 nodes of instance types larger than
`f1-micro`, `g1-small`, or `n1-standard-1`.
1. Navigate to **{cloud-gear}** **Operations > Kubernetes**.
1. Navigate to **Operations > Kubernetes**.
1. In **Kubernetes Cluster**, select a cluster.
1. In the **Applications** section, find **Elastic Stack** and click **Install**.
@ -601,7 +601,7 @@ your data. Fluentd sends logs in syslog format.
To enable Fluentd:
1. Navigate to **{cloud-gear}** **Operations > Kubernetes** and click
1. Navigate to **Operations > Kubernetes** and click
**Applications**. You will be prompted to enter a host, port and protocol
where the WAF logs will be sent to via syslog.
1. Provide the host domain name or URL in **SIEM Hostname**.

View File

@ -19,6 +19,9 @@ To access the Compliance Dashboard for a group, navigate to **{shield}** **Secur
![Compliance Dashboard](img/compliance_dashboard_v13_3.png)
NOTE: **Note:**
The Compliance Dashboard shows only the latest MR on each project.
## Use cases
This feature is for people who care about the compliance status of projects within their group.

View File

@ -84,7 +84,7 @@ your cluster, which can cause deployment jobs to fail.
To clear the cache:
1. Navigate to your groups **{cloud-gear}** **Kubernetes** page,
1. Navigate to your groups **Kubernetes** page,
and select your cluster.
1. Expand the **Advanced settings** section.
1. Click **Clear cluster cache**.

View File

@ -7,13 +7,12 @@ type: reference, howto, concepts
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/2772) in GitLab 9.0.
GitLab supports up to 20 levels of subgroups, also known as nested groups or hierarchical groups.
levels of groups.
By using subgroups you can do the following:
- **Separate internal / external organizations.** Since every group
can have its own visibility level, you are able to host groups for different
purposes under the same umbrella.
can have its own visibility level ([public, internal, or private](../../../development/permissions.md#general-permissions)),
you're able to host groups for different purposes under the same umbrella.
- **Organize large projects.** For large projects, subgroups makes it
potentially easier to separate permissions on parts of the source code.
- **Make it easier to manage people and control visibility.** Give people

View File

@ -4,9 +4,9 @@ group: Package
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
---
# Dependency Proxy **(PREMIUM ONLY)**
# Dependency Proxy **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
NOTE: **Note:**
This is the user guide. In order to use the dependency proxy, an administrator
@ -82,6 +82,6 @@ for more details.
The following limitations apply:
- Only public groups are supported (authentication is not supported yet).
- Only [public groups are supported](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) (authentication is not supported yet).
- Only Docker Hub is supported.
- This feature requires Docker Hub being available.

View File

@ -56,9 +56,9 @@ Generate an access key for the IAM user, and configure GitLab with the credentia
To create and add a new Kubernetes cluster to your project, group, or instance:
1. Navigate to your:
- Project's **{cloud-gear}** **Operations > Kubernetes** page, for a project-level cluster.
- Group's **{cloud-gear}** **Kubernetes** page, for a group-level cluster.
- **Admin Area >** **{cloud-gear}** **Kubernetes**, for an instance-level cluster.
- Project's **Operations > Kubernetes** page, for a project-level cluster.
- Group's **Kubernetes** page, for a group-level cluster.
- **Admin Area > Kubernetes**, for an instance-level cluster.
1. Click **Add Kubernetes cluster**.
1. Under the **Create new cluster** tab, click **Amazon EKS**. You will be provided with an
`Account ID` and `External ID` to use in the next step.

View File

@ -25,14 +25,12 @@ module Banzai
def initialize(doc, context = nil, result = nil)
super
if update_nodes_enabled?
@new_nodes = {}
@nodes = self.result[:reference_filter_nodes]
end
@new_nodes = {}
@nodes = self.result[:reference_filter_nodes]
end
def call_and_update_nodes
update_nodes_enabled? ? with_update_nodes { call } : call
with_update_nodes { call }
end
# Returns a data attribute String to attach to a reference link
@ -165,11 +163,7 @@ module Banzai
end
def replace_text_with_html(node, index, html)
if update_nodes_enabled?
replace_and_update_new_nodes(node, index, html)
else
node.replace(html)
end
replace_and_update_new_nodes(node, index, html)
end
def replace_and_update_new_nodes(node, index, html)
@ -209,10 +203,6 @@ module Banzai
end
result[:reference_filter_nodes] = nodes
end
def update_nodes_enabled?
Feature.enabled?(:update_nodes_for_banzai_reference_filter, project)
end
end
end
end

View File

@ -239,6 +239,11 @@ msgid_plural "%d personal projects will be removed and cannot be restored."
msgstr[0] ""
msgstr[1] ""
msgid "%d previously merged commit"
msgid_plural "%d previously merged commits"
msgstr[0] ""
msgstr[1] ""
msgid "%d project"
msgid_plural "%d projects"
msgstr[0] ""
@ -1528,9 +1533,15 @@ msgstr ""
msgid "Add new directory"
msgstr ""
msgid "Add or remove previously merged commits"
msgstr ""
msgid "Add or subtract spent time"
msgstr ""
msgid "Add previously merged commits"
msgstr ""
msgid "Add reaction"
msgstr ""
@ -1576,6 +1587,15 @@ msgstr ""
msgid "Add webhook"
msgstr ""
msgid "Add/remove"
msgstr ""
msgid "AddContextCommits|Add previously merged commits"
msgstr ""
msgid "AddContextCommits|Add/remove"
msgstr ""
msgid "AddMember|No users specified."
msgstr ""
@ -6140,6 +6160,9 @@ msgstr ""
msgid "Commits to"
msgstr ""
msgid "Commits you select appear here. Go to the first tab and select commits to add to this merge request."
msgstr ""
msgid "Commits|An error occurred while fetching merge requests data."
msgstr ""
@ -6641,6 +6664,15 @@ msgstr ""
msgid "Contents of .gitlab-ci.yml"
msgstr ""
msgid "ContextCommits|Failed to create context commits. Please try again."
msgstr ""
msgid "ContextCommits|Failed to create/remove context commits. Please try again."
msgstr ""
msgid "ContextCommits|Failed to delete context commits. Please try again."
msgstr ""
msgid "Continue"
msgstr ""
@ -12830,9 +12862,6 @@ msgstr ""
msgid "Includes an MVC structure, mvnw and pom.xml to help you get started."
msgstr ""
msgid "Includes repository storage, wiki storage, LFS objects, build artifacts and packages. 0 for unlimited."
msgstr ""
msgid "Incoming email"
msgstr ""
@ -14648,9 +14677,6 @@ msgstr ""
msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
msgstr ""
msgid "Maximum namespace storage (MB)"
msgstr ""
msgid "Maximum number of %{name} (%{count}) exceeded"
msgstr ""
@ -16046,6 +16072,9 @@ msgstr ""
msgid "No child epics match applied filters"
msgstr ""
msgid "No commits present here"
msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@ -19573,6 +19602,9 @@ msgstr ""
msgid "Push an existing folder"
msgstr ""
msgid "Push commits to the source branch or add previously merged commits to review them."
msgstr ""
msgid "Push events"
msgstr ""
@ -20941,6 +20973,9 @@ msgstr ""
msgid "Search by author"
msgstr ""
msgid "Search by commit title or SHA"
msgstr ""
msgid "Search by message"
msgstr ""
@ -21591,6 +21626,9 @@ msgstr ""
msgid "Select user"
msgstr ""
msgid "Selected commits"
msgstr ""
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
msgstr ""
@ -24158,6 +24196,9 @@ msgstr ""
msgid "There are no closed merge requests"
msgstr ""
msgid "There are no commits yet."
msgstr ""
msgid "There are no custom project templates set up for this GitLab instance. They are enabled from GitLab's Admin Area. Contact your GitLab instance administrator to setup custom project templates."
msgstr ""
@ -25704,6 +25745,9 @@ msgstr ""
msgid "Unable to generate new instance ID"
msgstr ""
msgid "Unable to load commits. Try again later."
msgstr ""
msgid "Unable to load file contents. Try again later."
msgstr ""
@ -28050,6 +28094,9 @@ msgstr ""
msgid "Your search didn't match any commits."
msgstr ""
msgid "Your search didn't match any commits. Try a different query."
msgstr ""
msgid "Your subscription expired!"
msgstr ""

View File

@ -204,7 +204,7 @@ module QA
alias_method :to_s, :response
def success?
exitstatus.zero?
exitstatus == 0
end
end

View File

@ -14,13 +14,13 @@ module QA
end
def finished_all_axios_requests?
Capybara.page.evaluate_script('window.pendingRequests || 0').zero?
Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate
end
def finished_all_ajax_requests?
return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"')
Capybara.page.evaluate_script('jQuery.active').zero?
Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate
end
def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME)

View File

@ -17,7 +17,7 @@ RSpec.describe 'mail_room.yml' do
cmd = "puts ERB.new(File.read(#{absolute_path(mailroom_config_path).inspect})).result"
output, status = Gitlab::Popen.popen(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars)
raise "Error interpreting #{mailroom_config_path}: #{output}" unless status.zero?
raise "Error interpreting #{mailroom_config_path}: #{output}" unless status == 0
YAML.load(output)
end

View File

@ -105,22 +105,6 @@ RSpec.describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.minimum_password_length).to eq(10)
end
it 'updates namespace_storage_size_limit setting' do
put :update, params: { application_setting: { namespace_storage_size_limit: '100' } }
expect(response).to redirect_to(general_admin_application_settings_path)
expect(response).to set_flash[:notice].to('Application settings saved successfully')
expect(ApplicationSetting.current.namespace_storage_size_limit).to eq(100)
end
it 'does not accept an invalid namespace_storage_size_limit' do
put :update, params: { application_setting: { namespace_storage_size_limit: '-100' } }
expect(response).to render_template(:general)
expect(assigns(:application_setting).errors[:namespace_storage_size_limit]).to be_present
expect(ApplicationSetting.current.namespace_storage_size_limit).not_to eq(-100)
end
it 'updates repository_storages_weighted setting' do
put :update, params: { application_setting: { repository_storages_weighted_default: 75 } }

View File

@ -60,7 +60,7 @@ RSpec.describe Groups::Settings::RepositoryController do
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
end
}
end

View File

@ -77,7 +77,7 @@ RSpec.describe Projects::Settings::RepositoryController do
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
end
}
end

View File

@ -23,7 +23,7 @@ FactoryBot.define do
end
create_versions = ->(design, evaluator, commit_version) do
unless evaluator.versions_count.zero?
unless evaluator.versions_count == 0
project = design.project
issue = design.issue
repository = project.design_repository

View File

@ -40,7 +40,7 @@ FactoryBot.define do
)
version.designs += specific_designs
unless evaluator.designs_count.zero? || version.designs.present?
unless evaluator.designs_count == 0 || version.designs.present?
version.designs << create(:design, issue: version.issue)
end
end

View File

@ -36,7 +36,7 @@ RSpec.describe 'Contributions Calendar', :js do
def get_cell_date_selector(contributions, date)
contribution_text =
if contributions.zero?
if contributions == 0
'No contributions'
else
"#{contributions} #{'contribution'.pluralize(contributions)}"

View File

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-modal-stub
body-class="add-review-item pt-0"
cancel-variant="light"
modalclass=""
modalid="add-review-item"
ok-disabled="true"
ok-title="Save changes"
scrollable="true"
size="md"
title="Add or remove previously merged commits"
titletag="h4"
>
<gl-tabs-stub
contentclass="pt-0"
theme="indigo"
value="0"
>
<gl-tab-stub>
<div
class="mt-2"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
placeholder="Search by commit title or SHA"
value=""
/>
<review-tab-container-stub
commits=""
emptylisttext="Your search didn't match any commits. Try a different query."
loadingfailedtext="Unable to load commits. Try again later."
/>
</div>
</gl-tab-stub>
<gl-tab-stub>
<review-tab-container-stub
commits=""
emptylisttext="Commits you select appear here. Go to the first tab and select commits to add to this merge request."
loadingfailedtext="Unable to load commits. Try again later."
/>
</gl-tab-stub>
</gl-tabs-stub>
</gl-modal-stub>
`;

View File

@ -0,0 +1,174 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
import defaultState from '~/add_context_commits_modal/store/state';
import mutations from '~/add_context_commits_modal/store/mutations';
import * as actions from '~/add_context_commits_modal/store/actions';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('AddContextCommitsModal', () => {
let wrapper;
let store;
const createContextCommits = jest.fn();
const removeContextCommits = jest.fn();
const resetModalState = jest.fn();
const searchCommits = jest.fn();
const { commit } = getDiffWithCommit();
const createWrapper = (props = {}) => {
store = new Vuex.Store({
mutations,
state: {
...defaultState(),
},
actions: {
...actions,
searchCommits,
createContextCommits,
removeContextCommits,
resetModalState,
},
});
wrapper = shallowMount(AddReviewItemsModal, {
localVue,
store,
propsData: {
contextCommitsPath: '',
targetBranch: 'master',
mergeRequestIid: 1,
projectId: 1,
...props,
},
});
return wrapper;
};
const findModal = () => wrapper.find(GlModal);
const findSearch = () => wrapper.find(GlSearchBoxByType);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders modal with 2 tabs', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('an ok button labeled "Save changes"', () => {
expect(findModal().attributes('ok-title')).toEqual('Save changes');
});
describe('when in first tab, renders a modal with', () => {
it('renders the search box component', () => {
expect(findSearch().exists()).toBe(true);
});
it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
const searchText = 'abcd';
findSearch().vm.$emit('input', searchText);
expect(searchCommits).not.toBeCalled();
jest.advanceTimersByTime(500);
expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText, undefined);
});
it('disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('enabled ok button when atleast one row is selected', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
});
describe('when in second tab, renders a modal with', () => {
beforeEach(() => {
wrapper.vm.$store.state.tabIndex = 1;
});
it('a disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('an enabled ok button when atleast one row is selected', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
it('a disabled ok button in first tab, when row is selected in second tab', () => {
createWrapper({ selectedContextCommits: [commit] });
expect(wrapper.find(GlModal).attributes('ok-disabled')).toBe('true');
});
});
describe('has an ok button when clicked calls action', () => {
it('"createContextCommits" when only new commits to be added ', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(
expect.anything(),
{ commits: [{ ...commit, isSelected: true }], forceReload: true },
undefined,
);
});
});
it('"removeContextCommits" when only added commits are to be removed ', () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true, undefined);
});
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(
expect.anything(),
{ commits: [{ ...commit, isSelected: true }] },
undefined,
);
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
});
describe('has a cancel button when clicked', () => {
it('does not call "createContextCommits" or "removeContextCommits"', () => {
findModal().vm.$emit('cancel');
expect(createContextCommits).not.toHaveBeenCalled();
expect(removeContextCommits).not.toHaveBeenCalled();
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('cancel');
expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
describe('when model is closed by clicking the "X" button or by pressing "ESC" key', () => {
it('does not call "createContextCommits" or "removeContextCommits"', () => {
findModal().vm.$emit('close');
expect(createContextCommits).not.toHaveBeenCalled();
expect(removeContextCommits).not.toHaveBeenCalled();
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('close');
expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
});

View File

@ -0,0 +1,51 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import CommitItem from '~/diffs/components/commit_item.vue';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('ReviewTabContainer', () => {
let wrapper;
const { commit } = getDiffWithCommit();
const createWrapper = (props = {}) => {
wrapper = shallowMount(ReviewTabContainer, {
propsData: {
tab: 'commits',
isLoading: false,
loadingError: false,
loadingFailedText: 'Failed to load commits',
commits: [],
selectedRow: [],
...props,
},
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('shows loading icon when commits are being loaded', () => {
createWrapper({ isLoading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('shows loading error text when API call fails', () => {
createWrapper({ loadingError: true });
expect(wrapper.text()).toContain('Failed to load commits');
});
it('shows "No commits present here" when commits are not present', () => {
expect(wrapper.text()).toContain('No commits present here');
});
it('renders all passed commits as list', () => {
createWrapper({ commits: [commit] });
expect(wrapper.findAll(CommitItem).length).toBe(1);
});
});

View File

@ -0,0 +1,239 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setBaseConfig,
setTabIndex,
setCommits,
createContextCommits,
fetchContextCommits,
setContextCommits,
removeContextCommits,
setSelectedCommits,
setSearchText,
setToRemoveCommits,
resetModalState,
} from '~/add_context_commits_modal/store/actions';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from '../../helpers/vuex_action_helper';
describe('AddContextCommitsModalStoreActions', () => {
const contextCommitEndpoint =
'/api/v4/projects/gitlab-org%2fgitlab/merge_requests/1/context_commits';
const mergeRequestIid = 1;
const projectId = 1;
const projectPath = 'gitlab-org/gitlab';
const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
const dummyCommit = {
id: 1,
title: 'dummy commit',
short_id: 'abcdef',
committed_date: '2020-06-12',
};
gon.api_version = 'v4';
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('setBaseConfig', () => {
it('commits SET_BASE_CONFIG', done => {
const options = { contextCommitsPath, mergeRequestIid, projectId };
testAction(
setBaseConfig,
options,
{
contextCommitsPath: '',
mergeRequestIid,
projectId,
},
[
{
type: types.SET_BASE_CONFIG,
payload: options,
},
],
[],
done,
);
});
});
describe('setTabIndex', () => {
it('commits SET_TABINDEX', done => {
testAction(
setTabIndex,
{ tabIndex: 1 },
{ tabIndex: 0 },
[{ type: types.SET_TABINDEX, payload: { tabIndex: 1 } }],
[],
done,
);
});
});
describe('setCommits', () => {
it('commits SET_COMMITS', done => {
testAction(
setCommits,
{ commits: [], silentAddition: false },
{ isLoadingCommits: false, commits: [] },
[{ type: types.SET_COMMITS, payload: [] }],
[],
done,
);
});
it('commits SET_COMMITS_SILENT', done => {
testAction(
setCommits,
{ commits: [], silentAddition: true },
{ isLoadingCommits: true, commits: [] },
[{ type: types.SET_COMMITS_SILENT, payload: [] }],
[],
done,
);
});
});
describe('createContextCommits', () => {
it('calls API to create context commits', done => {
mock.onPost(contextCommitEndpoint).reply(200, {});
testAction(createContextCommits, { commits: [] }, {}, [], [], done);
createContextCommits(
{ state: { projectId, mergeRequestIid }, commit: () => null },
{ commits: [] },
)
.then(() => {
done();
})
.catch(done.fail);
});
});
describe('fetchContextCommits', () => {
beforeEach(() => {
mock
.onGet(
`/api/${gon.api_version}/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits`,
)
.reply(200, [dummyCommit]);
});
it('commits FETCH_CONTEXT_COMMITS', done => {
const contextCommit = { ...dummyCommit, isSelected: true };
testAction(
fetchContextCommits,
null,
{
mergeRequestIid,
projectId: projectPath,
isLoadingContextCommits: false,
contextCommitsLoadingError: false,
commits: [],
},
[{ type: types.FETCH_CONTEXT_COMMITS }],
[
{ type: 'setContextCommits', payload: [contextCommit] },
{ type: 'setCommits', payload: { commits: [contextCommit], silentAddition: true } },
{ type: 'setSelectedCommits', payload: [contextCommit] },
],
done,
);
});
});
describe('setContextCommits', () => {
it('commits SET_CONTEXT_COMMITS', done => {
testAction(
setContextCommits,
{ data: [] },
{ contextCommits: [], isLoadingContextCommits: false },
[{ type: types.SET_CONTEXT_COMMITS, payload: { data: [] } }],
[],
done,
);
});
});
describe('removeContextCommits', () => {
beforeEach(() => {
mock
.onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
.reply(204);
});
it('calls API to remove context commits', done => {
testAction(
removeContextCommits,
{ forceReload: false },
{ mergeRequestIid, projectId, toRemoveCommits: [] },
[],
[],
done,
);
});
});
describe('setSelectedCommits', () => {
it('commits SET_SELECTED_COMMITS', done => {
testAction(
setSelectedCommits,
[dummyCommit],
{ selectedCommits: [] },
[{ type: types.SET_SELECTED_COMMITS, payload: [dummyCommit] }],
[],
done,
);
});
});
describe('setSearchText', () => {
it('commits SET_SEARCH_TEXT', done => {
const searchText = 'Dummy Text';
testAction(
setSearchText,
searchText,
{ searchText: '' },
[{ type: types.SET_SEARCH_TEXT, payload: searchText }],
[],
done,
);
});
});
describe('setToRemoveCommits', () => {
it('commits SET_TO_REMOVE_COMMITS', done => {
const commitId = 'abcde';
testAction(
setToRemoveCommits,
[commitId],
{ toRemoveCommits: [] },
[{ type: types.SET_TO_REMOVE_COMMITS, payload: [commitId] }],
[],
done,
);
});
});
describe('resetModalState', () => {
it('commits RESET_MODAL_STATE', done => {
const commitId = 'abcde';
testAction(
resetModalState,
null,
{ toRemoveCommits: [commitId] },
[{ type: types.RESET_MODAL_STATE }],
[],
done,
);
});
});
});

View File

@ -0,0 +1,156 @@
import mutations from '~/add_context_commits_modal/store/mutations';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import { TEST_HOST } from 'helpers/test_constants';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('AddContextCommitsModalStoreMutations', () => {
const { commit } = getDiffWithCommit();
describe('SET_BASE_CONFIG', () => {
it('should set contextCommitsPath, mergeRequestIid and projectId', () => {
const state = {};
const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
const mergeRequestIid = 1;
const projectId = 1;
mutations[types.SET_BASE_CONFIG](state, { contextCommitsPath, mergeRequestIid, projectId });
expect(state.contextCommitsPath).toEqual(contextCommitsPath);
expect(state.mergeRequestIid).toEqual(mergeRequestIid);
expect(state.projectId).toEqual(projectId);
});
});
describe('SET_TABINDEX', () => {
it('sets tabIndex to specific index', () => {
const state = { tabIndex: 0 };
mutations[types.SET_TABINDEX](state, 1);
expect(state.tabIndex).toBe(1);
});
});
describe('FETCH_COMMITS', () => {
it('sets isLoadingCommits to true', () => {
const state = { isLoadingCommits: false };
mutations[types.FETCH_COMMITS](state);
expect(state.isLoadingCommits).toBe(true);
});
});
describe('SET_COMMITS', () => {
it('sets commits to passed data and stop loading', () => {
const state = { commits: [], isLoadingCommits: true };
mutations[types.SET_COMMITS](state, [commit]);
expect(state.commits).toStrictEqual([commit]);
expect(state.isLoadingCommits).toBe(false);
});
});
describe('SET_COMMITS_SILENT', () => {
it('sets commits to passed data and loading continues', () => {
const state = { commits: [], isLoadingCommits: true };
mutations[types.SET_COMMITS_SILENT](state, [commit]);
expect(state.commits).toStrictEqual([commit]);
expect(state.isLoadingCommits).toBe(true);
});
});
describe('FETCH_COMMITS_ERROR', () => {
it('sets commitsLoadingError to true', () => {
const state = { commitsLoadingError: false };
mutations[types.FETCH_COMMITS_ERROR](state);
expect(state.commitsLoadingError).toBe(true);
});
});
describe('FETCH_CONTEXT_COMMITS', () => {
it('sets isLoadingContextCommits to true', () => {
const state = { isLoadingContextCommits: false };
mutations[types.FETCH_CONTEXT_COMMITS](state);
expect(state.isLoadingContextCommits).toBe(true);
});
});
describe('SET_CONTEXT_COMMITS', () => {
it('sets contextCommit to passed data and stop loading', () => {
const state = { contextCommits: [], isLoadingContextCommits: true };
mutations[types.SET_CONTEXT_COMMITS](state, [commit]);
expect(state.contextCommits).toStrictEqual([commit]);
expect(state.isLoadingContextCommits).toBe(false);
});
});
describe('FETCH_CONTEXT_COMMITS_ERROR', () => {
it('sets contextCommitsLoadingError to true', () => {
const state = { contextCommitsLoadingError: false };
mutations[types.FETCH_CONTEXT_COMMITS_ERROR](state);
expect(state.contextCommitsLoadingError).toBe(true);
});
});
describe('SET_SELECTED_COMMITS', () => {
it('sets selectedCommits to specified value', () => {
const state = { selectedCommits: [] };
mutations[types.SET_SELECTED_COMMITS](state, [commit]);
expect(state.selectedCommits).toStrictEqual([commit]);
});
});
describe('SET_SEARCH_TEXT', () => {
it('sets searchText to specified value', () => {
const searchText = 'Test';
const state = { searchText: '' };
mutations[types.SET_SEARCH_TEXT](state, searchText);
expect(state.searchText).toBe(searchText);
});
});
describe('SET_TO_REMOVE_COMMITS', () => {
it('sets searchText to specified value', () => {
const state = { toRemoveCommits: [] };
mutations[types.SET_TO_REMOVE_COMMITS](state, [commit.short_id]);
expect(state.toRemoveCommits).toStrictEqual([commit.short_id]);
});
});
describe('RESET_MODAL_STATE', () => {
it('sets searchText to specified value', () => {
const state = {
commits: [commit],
contextCommits: [commit],
selectedCommits: [commit],
toRemoveCommits: [commit.short_id],
searchText: 'Test',
};
mutations[types.RESET_MODAL_STATE](state);
expect(state.commits).toStrictEqual([]);
expect(state.contextCommits).toStrictEqual([]);
expect(state.selectedCommits).toStrictEqual([]);
expect(state.toRemoveCommits).toStrictEqual([]);
expect(state.searchText).toBe('');
});
});
});

View File

@ -667,6 +667,79 @@ describe('Api', () => {
});
});
describe('createContextCommits', () => {
it('creates a new context commit', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
const expectedData = {
commits: commitsData,
};
jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, [
{
id: 'abcdefghijklmnop',
short_id: 'abcdefg',
title: 'Dummy commit',
},
]);
Api.createContextCommits(projectPath, mergeRequestId, expectedData)
.then(({ data }) => {
expect(data[0].title).toBe('Dummy commit');
})
.then(done)
.catch(done.fail);
});
});
describe('allContextCommits', () => {
it('gets all context commits', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
jest.spyOn(axios, 'get');
mock
.onGet(expectedUrl)
.replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
Api.allContextCommits(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data[0].title).toBe('Dummy commit title');
})
.then(done)
.catch(done.fail);
});
});
describe('removeContextCommits', () => {
it('removes context commits', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
const expectedData = {
commits: commitsData,
};
jest.spyOn(axios, 'delete');
mock.onDelete(expectedUrl).replyOnce(204);
Api.removeContextCommits(projectPath, mergeRequestId, expectedData)
.then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
})
.then(done)
.catch(done.fail);
});
});
describe('release-related methods', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const dummyTagName = 'v1.3';

View File

@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
@ -12,7 +13,7 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
describe('Linked pipeline', () => {
let wrapper;
const findButton = () => wrapper.find('button');
const findButton = () => wrapper.find(GlButton);
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
@ -42,9 +43,7 @@ describe('Linked pipeline', () => {
});
it('should render a button', () => {
const linkElement = wrapper.find('.js-linked-pipeline-content');
expect(linkElement.exists()).toBe(true);
expect(findButton().exists()).toBe(true);
});
it('should render the project name', () => {
@ -62,7 +61,7 @@ describe('Linked pipeline', () => {
});
it('should have a ci-status child component', () => {
expect(wrapper.find('.js-linked-pipeline-status').exists()).toBe(true);
expect(wrapper.find(CiStatus).exists()).toBe(true);
});
it('should render the pipeline id', () => {
@ -77,15 +76,14 @@ describe('Linked pipeline', () => {
});
it('should render the tooltip text as the title attribute', () => {
const tooltipRef = wrapper.find('.js-linked-pipeline-content');
const titleAttr = tooltipRef.attributes('title');
const titleAttr = findButton().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
it('does not render the loading icon when isLoading is false', () => {
expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false);
it('sets the loading prop to false', () => {
expect(findButton().props('loading')).toBe(false);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@ -132,8 +130,8 @@ describe('Linked pipeline', () => {
createWrapper(props);
});
it('renders a loading icon', () => {
expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(true);
it('sets the loading prop to true', () => {
expect(findButton().props('loading')).toBe(true);
});
});

View File

@ -44,7 +44,8 @@ RSpec.describe EnvironmentsHelper do
'prometheus-alerts-available' => 'true',
'custom-dashboard-base-path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT,
'operations-settings-path' => project_settings_operations_path(project),
'can-access-operations-settings' => 'true'
'can-access-operations-settings' => 'true',
'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
)
end

View File

@ -110,20 +110,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
expect(filter.instance_variable_get(:@new_nodes)).to eq({ index => [filter.each_node.to_a[index]] })
end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it 'does not call replace_and_update_new_nodes' do
expect(filter).not_to receive(:replace_and_update_new_nodes).with(filter.nodes[index], index, html)
filter.send(method_name, *args) do
html
end
end
end
end
end
@ -198,49 +184,20 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
end
describe "#call_and_update_nodes" do
context "with update_nodes_for_banzai_reference_filter feature flag enabled" do
include_context 'new nodes'
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:filter) { described_class.new(document, project: project) }
include_context 'new nodes'
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:filter) { described_class.new(document, project: project) }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: true)
end
it "updates all new nodes", :aggregate_failures do
filter.instance_variable_set('@nodes', nodes)
it "updates all new nodes", :aggregate_failures do
filter.instance_variable_set('@nodes', nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).to receive(:with_update_nodes).and_call_original
expect(filter).to receive(:update_nodes!).and_call_original
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).to receive(:with_update_nodes).and_call_original
expect(filter).to receive(:update_nodes!).and_call_original
filter.call_and_update_nodes
filter.call_and_update_nodes
expect(filter.result[:reference_filter_nodes]).to eq(expected_nodes)
end
end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
include_context 'new nodes'
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it "does not change nodes", :aggregate_failures do
document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
filter = described_class.new(document, project: project)
filter.instance_variable_set('@nodes', nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).not_to receive(:with_update_nodes)
expect(filter).not_to receive(:update_nodes!)
filter.call_and_update_nodes
expect(filter.nodes).to eq(nodes)
expect(filter.result[:reference_filter_nodes]).to be nil
end
expect(filter.result[:reference_filter_nodes]).to eq(expected_nodes)
end
end
@ -251,10 +208,6 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
let(:result) { { reference_filter_nodes: nodes } }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: true)
end
it "updates all nodes", :aggregate_failures do
expect_next_instance_of(described_class) do |filter|
expect(filter).to receive(:call_and_update_nodes).and_call_original
@ -267,26 +220,5 @@ RSpec.describe Banzai::Filter::ReferenceFilter do
expect(result[:reference_filter_nodes]).to eq(expected_nodes)
end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
let(:result) { {} }
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
it "updates all nodes", :aggregate_failures do
expect_next_instance_of(described_class) do |filter|
expect(filter).to receive(:call_and_update_nodes).and_call_original
expect(filter).not_to receive(:with_update_nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
expect(filter).not_to receive(:update_nodes!)
end
described_class.call(document, { project: project }, result)
expect(result[:reference_filter_nodes]).to be nil
end
end
end
end

View File

@ -30,34 +30,6 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
described_class.call(markdown, project: project)
end
context "with update_nodes_for_banzai_reference_filter feature flag disabled" do
before do
stub_feature_flags(update_nodes_for_banzai_reference_filter: false)
end
context 'when shorthand pattern #ISSUE_ID is used' do
it 'links an internal issues and doesnt store nodes in result[:reference_filter_nodes]', :aggregate_failures do
issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}"
result = described_class.call(markdown, project: project)
link = result[:output].css('a').first
expect(link['href']).to eq(Gitlab::Routing.url_helpers.project_issue_path(project, issue))
expect(result[:reference_filter_nodes]).to eq nil
end
end
it 'execute :each_node for each reference_filter', :aggregate_failures do
issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}"
described_class.reference_filters do |reference_filter|
expect_any_instance_of(reference_filter).to receive(:each_node).once
end
described_class.call(markdown, project: project)
end
end
context 'when shorthand pattern #ISSUE_ID is used' do
it 'links an internal issue if it exists' do
issue = create(:issue, project: project)

View File

@ -18,7 +18,7 @@ RSpec.describe Gitlab::Git::DiffCollection, :seed_helper do
return enum_for(:each) unless block_given?
loop do
break if @count.zero?
break if @count == 0
# It is critical to decrement before yielding. We may never reach the lines after 'yield'.
@count -= 1

View File

@ -169,7 +169,7 @@ RSpec.describe Gitlab::GithubImport::Client do
expect(client).to receive(:raise_or_wait_for_rate_limit)
client.with_rate_limit do
if retries.zero?
if retries == 0
retries += 1
raise(Octokit::TooManyRequests)
end

View File

@ -118,7 +118,7 @@ RSpec.describe Gitlab::Popen::Runner do
stdout: 'stdout',
stderr: '',
exitstatus: 0,
status: double(exitstatus: exitstatus, success?: exitstatus.zero?),
status: double(exitstatus: exitstatus, success?: exitstatus == 0),
duration: 0.1)
result =

View File

@ -191,7 +191,7 @@ RSpec.describe Gitlab::TreeSummary do
with_them do
before do
create_file('dummy', path: 'other') if num_entries.zero?
create_file('dummy', path: 'other') if num_entries == 0
1.upto(num_entries) { |n| create_file(n, path: path) }
end
@ -218,7 +218,7 @@ RSpec.describe Gitlab::TreeSummary do
with_them do
before do
create_file('dummy', path: 'other') if num_entries.zero?
create_file('dummy', path: 'other') if num_entries == 0
1.upto(num_entries) { |n| create_file(n, path: path) }
end

View File

@ -87,11 +87,6 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value('abc').for(:minimum_password_length) }
it { is_expected.to allow_value(10).for(:minimum_password_length) }
it { is_expected.to allow_value(0).for(:namespace_storage_size_limit) }
it { is_expected.to allow_value(1).for(:namespace_storage_size_limit) }
it { is_expected.not_to allow_value(nil).for(:namespace_storage_size_limit) }
it { is_expected.not_to allow_value(-1).for(:namespace_storage_size_limit) }
it { is_expected.to allow_value(300).for(:issues_create_limit) }
it { is_expected.not_to allow_value('three').for(:issues_create_limit) }
it { is_expected.not_to allow_value(nil).for(:issues_create_limit) }

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects::Metrics::Dashboards::BuilderController' do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:user) { create(:user) }
def send_request(params = {})
post namespace_project_metrics_dashboards_builder_path(namespace_id: project.namespace, project_id: project, format: :json, **params)
end
describe 'POST /:namespace/:project/-/metrics/dashboards/builder' do
context 'as anonymous user' do
before do
stub_feature_flags(metrics_dashboard_new_panel_page: true)
end
it 'redirects to sign in' do
send_request
expect(response).to redirect_to(new_user_session_path)
end
end
context 'as user with reporter access' do
before do
stub_feature_flags(metrics_dashboard_new_panel_page: true)
project.add_guest(user)
login_as(user)
end
it 'returns not found' do
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'as logged in user' do
before do
project.add_developer(user)
login_as(user)
end
context 'metrics_dashboard_new_panel_page is enabled' do
before do
stub_feature_flags(metrics_dashboard_new_panel_page: true)
end
it 'returns success' do
send_request
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'metrics_dashboard_new_panel_page is disabled' do
before do
stub_feature_flags(metrics_dashboard_new_panel_page: false)
end
it 'returns not found' do
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end

View File

@ -44,7 +44,7 @@ class BareRepoOperations
yield stdin if block_given?
end
unless status.zero?
unless status == 0
if allow_failure
return []
else

View File

@ -21,7 +21,7 @@ module MemoryUsageHelper
def get_memory_usage
output, status = Gitlab::Popen.popen(%w(free -m))
abort "`free -m` return code is #{status}: #{output}" unless status.zero?
abort "`free -m` return code is #{status}: #{output}" unless status == 0
result = output.split("\n")[1].split(" ")[1..-1]
attrs = %i(m_total m_used m_free m_shared m_buffers_cache m_available).freeze

View File

@ -42,7 +42,7 @@ module WaitForRequests
private
def finished_all_rack_requests?
Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
Gitlab::Testing::RequestBlockerMiddleware.num_active_requests == 0
end
def finished_all_js_requests?
@ -53,12 +53,12 @@ module WaitForRequests
end
def finished_all_axios_requests?
Capybara.page.evaluate_script('window.pendingRequests || 0').zero?
Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate
end
def finished_all_ajax_requests?
return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"')
Capybara.page.evaluate_script('jQuery.active').zero?
Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate
end
end

View File

@ -44,7 +44,7 @@ module ExceedQueryLimitHelpers
def log_message
if expected.is_a?(ActiveRecord::QueryRecorder)
counts = count_queries(strip_marginalia_annotations(expected.log))
extra_queries = strip_marginalia_annotations(@recorder.log).reject { |query| counts[query] -= 1 unless counts[query].zero? }
extra_queries = strip_marginalia_annotations(@recorder.log).reject { |query| counts[query] -= 1 unless counts[query] == 0 }
extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" }
(['Extra queries:'] + extra_queries_display).join("\n\n")
@ -188,7 +188,7 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do
def expected_count_message
or_fewer_msg = "or fewer" if @or_fewer
threshold_msg = "(+/- #{threshold})" unless threshold.zero?
threshold_msg = "(+/- #{threshold})" unless threshold == 0
["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ')
end