Commit 1308dc5e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f0707f41
import api from '~/api'; import api from '~/api';
import { __ } from '~/locale';
import createFlash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
...@@ -10,7 +8,6 @@ export default { ...@@ -10,7 +8,6 @@ export default {
}, },
requestDataError({ commit }) { requestDataError({ commit }) {
commit(types.REQUEST_DATA_ERROR); commit(types.REQUEST_DATA_ERROR);
createFlash(__('An error occurred loading code navigation'));
}, },
fetchData({ commit, dispatch, state }) { fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA); commit(types.REQUEST_DATA);
......
...@@ -354,7 +354,7 @@ export default { ...@@ -354,7 +354,7 @@ export default {
<template> <template>
<div v-show="shouldShow"> <div v-show="shouldShow">
<div v-if="isLoading" class="loading"><gl-loading-icon /></div> <div v-if="isLoading" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane"> <div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions <compare-versions
:merge-request-diffs="mergeRequestDiffs" :merge-request-diffs="mergeRequestDiffs"
......
...@@ -11,7 +11,7 @@ const FLASH_TYPES = { ...@@ -11,7 +11,7 @@ const FLASH_TYPES = {
const hideFlash = (flashEl, fadeTransition = true) => { const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) { if (fadeTransition) {
Object.assign(flashEl.style, { Object.assign(flashEl.style, {
transition: 'opacity .3s', transition: 'opacity 0.15s',
opacity: '0', opacity: '0',
}); });
} }
......
...@@ -6,7 +6,6 @@ import CommitMessageField from './message_field.vue'; ...@@ -6,7 +6,6 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue'; import Actions from './actions.vue';
import SuccessMessage from './success_message.vue'; import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
...@@ -15,7 +14,6 @@ export default { ...@@ -15,7 +14,6 @@ export default {
CommitMessageField, CommitMessageField,
SuccessMessage, SuccessMessage,
}, },
mixins: [glFeatureFlagsMixin()],
data() { data() {
return { return {
isCompact: true, isCompact: true,
...@@ -29,12 +27,8 @@ export default { ...@@ -29,12 +27,8 @@ export default {
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() { overviewText() {
return sprintf( return sprintf(
this.glFeatures.stageAllByDefault __(
? __(
'<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes', '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
)
: __(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
), ),
{ {
stagedFilesLength: this.stagedFiles.length, stagedFilesLength: this.stagedFiles.length,
......
...@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue'; import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'FileRowExtra', name: 'FileRowExtra',
...@@ -19,7 +18,6 @@ export default { ...@@ -19,7 +18,6 @@ export default {
ChangedFileIcon, ChangedFileIcon,
MrFileIcon, MrFileIcon,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
file: { file: {
type: Object, type: Object,
...@@ -57,15 +55,10 @@ export default { ...@@ -57,15 +55,10 @@ export default {
return n__('%d staged change', '%d staged changes', this.folderStagedCount); return n__('%d staged change', '%d staged changes', this.folderStagedCount);
} }
return sprintf( return sprintf(__('%{staged} staged and %{unstaged} unstaged changes'), {
this.glFeatures.stageAllByDefault
? __('%{staged} staged and %{unstaged} unstaged changes')
: __('%{unstaged} unstaged and %{staged} staged changes'),
{
unstaged: this.folderUnstagedCount, unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount, staged: this.folderStagedCount,
}, });
);
}, },
showTreeChangesCount() { showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened; return this.isTree && this.changesCount > 0 && !this.file.opened;
......
...@@ -79,10 +79,7 @@ export const createTempEntry = ( ...@@ -79,10 +79,7 @@ export const createTempEntry = (
if (type === 'blob') { if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.TOGGLE_FILE_OPEN, file.path);
if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }); commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
else commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path); dispatch('setFileActive', file.path);
dispatch('triggerFilesChange'); dispatch('triggerFilesChange');
...@@ -250,9 +247,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, ...@@ -250,9 +247,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
if (isReset) { if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry); commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} else if (!isInChanges) { } else if (!isInChanges) {
if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) }); commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
else commit(types.ADD_FILE_TO_CHANGED, newPath);
} }
if (!newEntry.tempFile) { if (!newEntry.tempFile) {
......
...@@ -158,9 +158,7 @@ export const changeFileContent = ({ commit, state, getters }, { path, content }) ...@@ -158,9 +158,7 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path); const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) { if (file.changed && indexOfChangedFile === -1) {
if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) }); commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
else commit(types.ADD_FILE_TO_CHANGED, path);
} else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) { } else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
} }
......
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
export default {
name: 'GroupEmptyState',
components: {
GlEmptyState,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config']),
},
};
</script>
<template>
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images available in this group')"
:svg-path="config.noContainersImage"
class="container-message"
>
<template #description>
<p class="js-no-container-images-text">
<gl-sprintf
:message="
s__(
`ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-empty-state>
</template>
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { mapState } from 'vuex';
export default {
name: 'ProjectEmptyState',
components: {
ClipboardButton,
GlEmptyState,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config']),
dockerBuildCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker build -t ${this.config.repositoryUrl} .`;
},
dockerPushCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker push ${this.config.repositoryUrl}`;
},
dockerLoginCommand() {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
return `docker login ${this.config.registryHostUrlWithPort}`;
},
},
};
</script>
<template>
<gl-empty-state
:title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="config.noContainersImage"
class="container-message"
>
<template #description>
<p class="js-no-container-images-text">
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
<p class="js-not-logged-in-to-registry-text">
<gl-sprintf
:message="
s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
the Container Registry by using your GitLab username and password. If you have
%{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
%{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
instead of a password.`)
"
>
<template #twofaDocLink="{content}">
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
</template>
<template #personalAccessTokensDocLink="{content}">
<gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</p>
<div class="input-group append-bottom-10">
<input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerLoginCommand"
:title="s__('ContainerRegistry|Copy login command')"
class="input-group-text"
/>
</span>
</div>
<p></p>
<p>
{{
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p>
<div class="input-group append-bottom-10">
<input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command')"
class="input-group-text"
/>
</span>
</div>
<div class="input-group">
<input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
<span class="input-group-append">
<clipboard-button
:text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command')"
class="input-group-text"
/>
</span>
</div>
</template>
</gl-empty-state>
</template>
<script> <script>
export default {}; import { mapState, mapActions } from 'vuex';
import {
GlLoadingIcon,
GlEmptyState,
GlPagination,
GlTooltipDirective,
GlButton,
GlIcon,
GlModal,
GlSprintf,
GlLink,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
export default {
name: 'RegistryListApp',
components: {
GlEmptyState,
GlLoadingIcon,
GlPagination,
ProjectEmptyState,
GroupEmptyState,
ClipboardButton,
GlButton,
GlIcon,
GlModal,
GlSprintf,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
data() {
return {
itemToDelete: {},
};
},
computed: {
...mapState(['config', 'isLoading', 'images', 'pagination']),
tracking() {
return {
label: 'registry_repository_delete',
};
},
currentPage: {
get() {
return this.pagination.page;
},
set(page) {
this.requestImagesList({ page });
},
},
},
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
deleteImage(item) {
// This event is already tracked in the system and so the name must be kept to aggregate the data
this.track('click_button');
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
handleDeleteRepository() {
this.track('confirm_delete');
this.requestDeleteImage(this.itemToDelete.destroy_path);
this.itemToDelete = {};
},
encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path });
return window.btoa(params);
},
},
};
</script> </script>
<template> <template>
<div></div> <div class="position-absolute w-100 slide-enter-from-element">
<gl-empty-state
v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')"
:svg-path="config.containersErrorImage"
>
<template #description>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-empty-state>
<template v-else>
<gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
<template v-else>
<div v-if="images.length" ref="imagesList">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p>
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<div class="d-flex flex-column">
<div
v-for="(listItem, index) in images"
:key="index"
ref="rowItem"
:class="[
'd-flex justify-content-between align-items-center py-2 border-bottom',
{ 'border-top': index === 0 },
]"
>
<div>
<router-link
ref="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
)
"
>
<gl-button
ref="deleteImageButton"
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
>
<gl-icon name="remove" />
</gl-button>
</div>
</div>
</div>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 mt-2"
/>
</div>
<template v-else>
<project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else />
</template>
</template>
<gl-modal
ref="deleteModal"
modal-id="delete-image-modal"
ok-variant="danger"
@ok="handleDeleteRepository"
@cancel="track('cancel_delete')"
>
<template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p>
<gl-sprintf
:message=" s__(
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
),"
>
<template #title>
<b>{{ itemToDelete.path }}</b>
</template>
</gl-sprintf>
</p>
<template #modal-ok>{{ __('Remove') }}</template>
</gl-modal>
</template>
</div>
</template> </template>
...@@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) ...@@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {})
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => { export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
const url = window.atob(id); const { tags_path } = JSON.parse(window.atob(id));
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination; const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios return axios
.get(url, { params: { page, per_page: perPage } }) .get(tags_path, { params: { page, per_page: perPage } })
.then(({ data, headers }) => { .then(({ data, headers }) => {
dispatch('receiveTagsListSuccess', { data, headers }); dispatch('receiveTagsListSuccess', { data, headers });
}) })
......
...@@ -5,6 +5,7 @@ export default { ...@@ -5,6 +5,7 @@ export default {
[types.SET_INITIAL_STATE](state, config) { [types.SET_INITIAL_STATE](state, config) {
state.config = { state.config = {
...config, ...config,
isGroupPage: config.isGroupPage !== undefined,
}; };
}, },
......
...@@ -3,10 +3,6 @@ ...@@ -3,10 +3,6 @@
class IdeController < ApplicationController class IdeController < ApplicationController
layout 'fullscreen' layout 'fullscreen'
before_action do
push_frontend_feature_flag(:stage_all_by_default, default_enabled: true)
end
def index def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end end
......
...@@ -186,7 +186,8 @@ module Ci ...@@ -186,7 +186,8 @@ module Ci
}, },
execute_params: { execute_params: {
ignore_skip_ci: true, ignore_skip_ci: true,
bridge: self bridge: self,
merge_request: parent_pipeline.merge_request
} }
} }
end end
......
...@@ -77,9 +77,7 @@ module Ci ...@@ -77,9 +77,7 @@ module Ci
validates :sha, presence: { unless: :importing? } validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? }
validates :merge_request, presence: { if: :merge_request_event? } validates :tag, inclusion: { in: [false], if: :merge_request? }
validates :merge_request, absence: { unless: :merge_request_event? }
validates :tag, inclusion: { in: [false], if: :merge_request_event? }
validates :external_pull_request, presence: { if: :external_pull_request_event? } validates :external_pull_request, presence: { if: :external_pull_request_event? }
validates :external_pull_request, absence: { unless: :external_pull_request_event? } validates :external_pull_request, absence: { unless: :external_pull_request_event? }
...@@ -662,7 +660,7 @@ module Ci ...@@ -662,7 +660,7 @@ module Ci
variables.concat(predefined_commit_variables) variables.concat(predefined_commit_variables)
if merge_request_event? && merge_request if merge_request?
variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s) variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s) variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s) variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
...@@ -720,7 +718,7 @@ module Ci ...@@ -720,7 +718,7 @@ module Ci
# All the merge requests for which the current pipeline runs/ran against # All the merge requests for which the current pipeline runs/ran against
def all_merge_requests def all_merge_requests
@all_merge_requests ||= @all_merge_requests ||=
if merge_request_event? if merge_request?
MergeRequest.where(id: merge_request_id) MergeRequest.where(id: merge_request_id)
else else
MergeRequest.where(source_project_id: project_id, source_branch: ref) MergeRequest.where(source_project_id: project_id, source_branch: ref)
...@@ -812,7 +810,7 @@ module Ci ...@@ -812,7 +810,7 @@ module Ci
# * nil: Modified path can not be evaluated # * nil: Modified path can not be evaluated
def modified_paths def modified_paths
strong_memoize(:modified_paths) do strong_memoize(:modified_paths) do
if merge_request_event? if merge_request?
merge_request.modified_paths merge_request.modified_paths
elsif branch_updated? elsif branch_updated?
push_details.modified_paths push_details.modified_paths
...@@ -836,12 +834,12 @@ module Ci ...@@ -836,12 +834,12 @@ module Ci
ref == project.default_branch ref == project.default_branch
end end
def triggered_by_merge_request? def merge_request?
merge_request_event? && merge_request_id.present? merge_request_id.present?
end end
def detached_merge_request_pipeline? def detached_merge_request_pipeline?
triggered_by_merge_request? && target_sha.nil? merge_request? && target_sha.nil?
end end
def legacy_detached_merge_request_pipeline? def legacy_detached_merge_request_pipeline?
...@@ -849,7 +847,7 @@ module Ci ...@@ -849,7 +847,7 @@ module Ci
end end
def merge_request_pipeline? def merge_request_pipeline?
triggered_by_merge_request? && target_sha.present? merge_request? && target_sha.present?
end end
def merge_request_ref? def merge_request_ref?
...@@ -865,7 +863,7 @@ module Ci ...@@ -865,7 +863,7 @@ module Ci
end end
def source_ref def source_ref
if triggered_by_merge_request? if merge_request?
merge_request.source_branch merge_request.source_branch
else else
ref ref
...@@ -885,7 +883,7 @@ module Ci ...@@ -885,7 +883,7 @@ module Ci
end end
def merge_request_event_type def merge_request_event_type
return unless merge_request_event? return unless merge_request?
strong_memoize(:merge_request_event_type) do strong_memoize(:merge_request_event_type) do
if merge_request_pipeline? if merge_request_pipeline?
...@@ -918,7 +916,7 @@ module Ci ...@@ -918,7 +916,7 @@ module Ci
def git_ref def git_ref
strong_memoize(:git_ref) do strong_memoize(:git_ref) do
if merge_request_event? if merge_request?
## ##
# In the future, we're going to change this ref to # In the future, we're going to change this ref to
# merge request's merged reference, such as "refs/merge-requests/:iid/merge". # merge request's merged reference, such as "refs/merge-requests/:iid/merge".
......
...@@ -11,7 +11,7 @@ module Ci ...@@ -11,7 +11,7 @@ module Ci
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
delegate :merge_request_event?, delegate :merge_request?,
:merge_request_ref?, :merge_request_ref?,
:legacy_detached_merge_request_pipeline?, :legacy_detached_merge_request_pipeline?,
:merge_train_pipeline?, to: :pipeline :merge_train_pipeline?, to: :pipeline
......
...@@ -7,7 +7,7 @@ module HasRef ...@@ -7,7 +7,7 @@ module HasRef
extend ActiveSupport::Concern extend ActiveSupport::Concern
def branch? def branch?
!tag? && !merge_request_event? !tag? && !merge_request?
end end
def git_ref def git_ref
......
...@@ -9,6 +9,8 @@ module ReactiveCaching ...@@ -9,6 +9,8 @@ module ReactiveCaching
ExceededReactiveCacheLimit = Class.new(StandardError) ExceededReactiveCacheLimit = Class.new(StandardError)
included do included do
extend ActiveModel::Naming
class_attribute :reactive_cache_key class_attribute :reactive_cache_key
class_attribute :reactive_cache_lease_timeout class_attribute :reactive_cache_lease_timeout
class_attribute :reactive_cache_refresh_interval class_attribute :reactive_cache_refresh_interval
......
...@@ -1163,7 +1163,7 @@ class MergeRequest < ApplicationRecord ...@@ -1163,7 +1163,7 @@ class MergeRequest < ApplicationRecord
# Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`), # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
# we cannot look up environments with source branch name. # we cannot look up environments with source branch name.
def environments def environments
return Environment.none unless actual_head_pipeline&.triggered_by_merge_request? return Environment.none unless actual_head_pipeline&.merge_request?
actual_head_pipeline.environments actual_head_pipeline.environments
end end
......
...@@ -61,6 +61,8 @@ class MergeRequest::Pipelines ...@@ -61,6 +61,8 @@ class MergeRequest::Pipelines
pipelines.joins(shas_table) pipelines.joins(shas_table)
end end
# NOTE: this method returns only parent merge request pipelines.
# Child merge request pipelines have a different source.
def triggered_by_merge_request def triggered_by_merge_request
source_project.ci_pipelines source_project.ci_pipelines
.where(source: :merge_request_event, merge_request: merge_request) .where(source: :merge_request_event, merge_request: merge_request)
......
...@@ -5,6 +5,9 @@ class WikiPage ...@@ -5,6 +5,9 @@ class WikiPage
PageChangedError = Class.new(StandardError) PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError) PageRenameError = Class.new(StandardError)
MAX_TITLE_BYTES = 245
MAX_DIRECTORY_BYTES = 255
include ActiveModel::Validations include ActiveModel::Validations
include ActiveModel::Conversion include ActiveModel::Conversion
include StaticModel include StaticModel
...@@ -51,6 +54,7 @@ class WikiPage ...@@ -51,6 +54,7 @@ class WikiPage
validates :title, presence: true validates :title, presence: true
validates :content, presence: true validates :content, presence: true
validate :validate_path_limits, if: :title_changed?
# The GitLab ProjectWiki instance. # The GitLab ProjectWiki instance.
attr_reader :wiki attr_reader :wiki
...@@ -262,7 +266,7 @@ class WikiPage ...@@ -262,7 +266,7 @@ class WikiPage
end end
def title_changed? def title_changed?
title.present? && self.class.unhyphenize(@page.url_path) != title title.present? && (@page.nil? || self.class.unhyphenize(@page.url_path) != title)
end end
# Updates the current @attributes hash by merging a hash of params # Updates the current @attributes hash by merging a hash of params
...@@ -324,4 +328,16 @@ class WikiPage ...@@ -324,4 +328,16 @@ class WikiPage
set_attributes set_attributes
@persisted = errors.blank? @persisted = errors.blank?
end end
def validate_path_limits
*dirnames, title = @attributes[:title].split('/')
if title.bytesize > MAX_TITLE_BYTES
errors.add(:title, _("exceeds the limit of %{bytes} bytes for page titles") % { bytes: MAX_TITLE_BYTES })
end
if dirnames.any? { |d| d.bytesize > MAX_DIRECTORY_BYTES }
errors.add(:title, _("exceeds the limit of %{bytes} bytes for directory names") % { bytes: MAX_DIRECTORY_BYTES })
end
end
end end
...@@ -116,7 +116,7 @@ module Ci ...@@ -116,7 +116,7 @@ module Ci
def merge_request_presenter def merge_request_presenter
strong_memoize(:merge_request_presenter) do strong_memoize(:merge_request_presenter) do
if pipeline.triggered_by_merge_request? if pipeline.merge_request?
pipeline.merge_request.present(current_user: current_user) pipeline.merge_request.present(current_user: current_user)
end end
end end
......
...@@ -25,7 +25,7 @@ class PipelineEntity < Grape::Entity ...@@ -25,7 +25,7 @@ class PipelineEntity < Grape::Entity
expose :flags do expose :flags do
expose :stuck?, as: :stuck expose :stuck?, as: :stuck
expose :auto_devops_source?, as: :auto_devops expose :auto_devops_source?, as: :auto_devops
expose :merge_request_event?, as: :merge_request expose :merge_request?, as: :merge_request
expose :has_yaml_errors?, as: :yaml_errors expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable expose :can_cancel?, as: :cancelable
...@@ -59,11 +59,11 @@ class PipelineEntity < Grape::Entity ...@@ -59,11 +59,11 @@ class PipelineEntity < Grape::Entity
expose :tag?, as: :tag expose :tag?, as: :tag
expose :branch?, as: :branch expose :branch?, as: :branch
expose :merge_request_event?, as: :merge_request expose :merge_request?, as: :merge_request
end end
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :merge_request_event_type, if: -> (pipeline, _) { pipeline.merge_request_event? } expose :merge_request_event_type, if: -> (pipeline, _) { pipeline.merge_request? }
expose :source_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? } expose :source_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? }
expose :target_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? } expose :target_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? }
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? } expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
...@@ -104,7 +104,7 @@ class PipelineEntity < Grape::Entity ...@@ -104,7 +104,7 @@ class PipelineEntity < Grape::Entity
end end
def has_presentable_merge_request? def has_presentable_merge_request?
pipeline.triggered_by_merge_request? && pipeline.merge_request? &&
can?(request.current_user, :read_merge_request, pipeline.merge_request) can?(request.current_user, :read_merge_request, pipeline.merge_request)
end end
......
...@@ -24,7 +24,7 @@ module MergeRequests ...@@ -24,7 +24,7 @@ module MergeRequests
## ##
# UpdateMergeRequestsWorker could be retried by an exception. # UpdateMergeRequestsWorker could be retried by an exception.
# pipelines for merge request should not be recreated in such case. # pipelines for merge request should not be recreated in such case.
return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.triggered_by_merge_request? return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.merge_request?
return false if merge_request.has_no_commits? return false if merge_request.has_no_commits?
true true
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
is_group_page: true,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
- else - else
#js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json), #js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json),
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
%button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
.editor-wrap{ ":class" => "classObject" } .editor-wrap{ ":class" => "classObject" }
.loading .loading
%i.fa.fa-spinner.fa-spin .spinner.spinner-md
.editor .editor
%pre{ "style" => "height: 350px" } %pre{ "style" => "height: 350px" }
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json), #conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } } resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
.loading{ "v-if" => "isLoading" } .loading{ "v-if" => "isLoading" }
%i.fa.fa-spinner.fa-spin .spinner.spinner-md
.nothing-here-block{ "v-if" => "hasError" } .nothing-here-block{ "v-if" => "hasError" }
{{conflictsData.errorMessage}} {{conflictsData.errorMessage}}
......
...@@ -30,7 +30,8 @@ ...@@ -30,7 +30,8 @@
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
.card-footer .card-footer
.text-center= icon('spinner spin', class: 'js-source-loading') .text-center
.js-source-loading.mt-1.spinner.spinner-sm
%ul.list-unstyled.mr_source_commit %ul.list-unstyled.mr_source_commit
.col-lg-6 .col-lg-6
...@@ -58,7 +59,8 @@ ...@@ -58,7 +59,8 @@
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
.card-footer .card-footer
.text-center= icon('spinner spin', class: "js-target-loading") .text-center
.js-target-loading.mt-1.spinner.spinner-sm
%ul.list-unstyled.mr_target_commit %ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any? - if @merge_request.errors.any?
......
...@@ -47,4 +47,5 @@ ...@@ -47,4 +47,5 @@
= render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true = render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
.mr-loading-status .mr-loading-status
= spinner .loading.hide
.spinner.spinner-md
...@@ -88,7 +88,8 @@ ...@@ -88,7 +88,8 @@
show_whitespace_default: @show_whitespace_default.to_s } show_whitespace_default: @show_whitespace_default.to_s }
.mr-loading-status .mr-loading-status
= spinner .loading.hide
.spinner.spinner-md
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
......
...@@ -17,9 +17,13 @@ ...@@ -17,9 +17,13 @@
= icon('lightbulb-o') = icon('lightbulb-o')
- if @page.persisted? - if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
target: '_blank', rel: 'noopener noreferrer'
- else - else
= s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/project/wiki/index', anchor: 'creating-a-new-wiki-page'),
target: '_blank', rel: 'noopener noreferrer'
.form-group.row .form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12 .col-sm-12
......
---
title: Replaced underscore with lodash for spec/javascripts/vue_shared/components
merge_request: 25018
author: Shubham Pandey
type: other
---
title: Limit length of wiki file/directory names
merge_request: 24364
author:
type: changed
---
title: Allow running child pipelines as merge request pipelines
merge_request: 23884
author:
type: fixed
---
title: Fix false matches of substitution-based quick actions in text
merge_request: 24699
author:
type: fixed
...@@ -119,3 +119,7 @@ This limit can be configured for self hosted installations when [enabling ...@@ -119,3 +119,7 @@ This limit can be configured for self hosted installations when [enabling
Elasticsearch](../integration/elasticsearch.md#enabling-elasticsearch). Elasticsearch](../integration/elasticsearch.md#enabling-elasticsearch).
NOTE: **Note:** Set the limit to `0` to disable it. NOTE: **Note:** Set the limit to `0` to disable it.
## Wiki limits
- [Length restrictions for file and directory names](../user/project/wiki/index.md#length-restrictions-for-file-and-directory-names).
...@@ -65,10 +65,13 @@ maximum memory threshold (in bytes) for the Unicorn worker killer by ...@@ -65,10 +65,13 @@ maximum memory threshold (in bytes) for the Unicorn worker killer by
setting the following values `/etc/gitlab/gitlab.rb`: setting the following values `/etc/gitlab/gitlab.rb`:
```ruby ```ruby
unicorn['worker_memory_limit_min'] = "400 * 1 << 20" unicorn['worker_memory_limit_min'] = "1024 * 1 << 20"
unicorn['worker_memory_limit_max'] = "650 * 1 << 20" unicorn['worker_memory_limit_max'] = "1280 * 1 << 20"
``` ```
NOTE: **Note:**
These values apply to GitLab 12.7.0 or newer versions. For older GitLab versions please consult [previous worker memory limits](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/12.6.0+ee.0/files/gitlab-cookbooks/gitlab/attributes/default.rb#L422-423).
Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MAX` Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MAX`
[environment variables](../environment_variables.md). [environment variables](../environment_variables.md).
......
...@@ -425,10 +425,12 @@ It is also possible to use GraphQL outside of Vue by directly importing ...@@ -425,10 +425,12 @@ It is also possible to use GraphQL outside of Vue by directly importing
and using the default client with queries. and using the default client with queries.
```javascript ```javascript
import defaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import query from './query.graphql'; import query from './query.graphql';
defaultClient.query(query) const defaultClient = createDefaultClient();
defaultClient.query({ query })
.then(result => console.log(result)); .then(result => console.log(result));
``` ```
......
...@@ -38,7 +38,7 @@ In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com ...@@ -38,7 +38,7 @@ In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com
Below is a diagram of the recommended architecture. Below is a diagram of the recommended architecture.
![AWS architecture diagram](img/aws_diagram.png) ![AWS architecture diagram](img/aws_ha_architecture_diagram.png)
## AWS costs ## AWS costs
...@@ -519,11 +519,34 @@ read the [repository storage paths docs](../../administration/repository_storage ...@@ -519,11 +519,34 @@ read the [repository storage paths docs](../../administration/repository_storage
### Setting up Gitaly ### Setting up Gitaly
Gitaly is a service that provides high-level RPC access to Git repositories. CAUTION: **Caution:** In this architecture, having a single Gitaly server creates a single point of failure. This limitation will be removed once [Gitaly HA](https://gitlab.com/groups/gitlab-org/-/epics/842) is released.
It should be enabled and configured in a separate EC2 instance on the
[private VPC](#subnets) we configured previously.
Follow the [documentation to set up Gitaly](../../administration/gitaly/index.md). Gitaly is a service that provides high-level RPC access to Git repositories.
It should be enabled and configured on a separate EC2 instance in one of the
[private subnets](#subnets) we configured previously.
Let's create an EC2 instance where we'll install Gitaly:
1. From the EC2 dashboard, click **Launch instance**.
1. Choose an AMI. In this example, we'll select the **Ubuntu Server 18.04 LTS (HVM), SSD Volume Type**.
1. Choose an instance type. We'll pick a **c5.xlarge**.
1. Click **Configure Instance Details**.
1. In the **Network** dropdown, select `gitlab-vpc`, the VPC we created earlier.
1. In the **Subnet** dropdown, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
1. Click **Add Storage**.
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisoned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisoned IOPS SSD (io1)`.
1. Click on **Add Tags** and add your tags. In our case, we'll only set `Key: Name` and `Value: Gitaly`.
1. Click on **Configure Security Group** and let's **Create a new security group**.
1. Give your security group a name and description. We'll use `gitlab-gitaly-sec-group` for both.
1. Create a **Custom TCP** rule and add port `8075` to the **Port Range**. For the **Source**, select the `gitlab-loadbalancer-sec-group`.
1. Click **Review and launch** followed by **Launch** if you're happy with your settings.
1. Finally, acknowledge that you have access to the selected private key file or create a new one. Click **Launch Instances**.
> **Optional:** Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above.
Now that we have our EC2 instance ready, follow the [documentation to install GitLab and set up Gitaly on its own server](../../administration/gitaly/index.md#running-gitaly-on-its-own-server).
### Using Amazon S3 object storage ### Using Amazon S3 object storage
......
...@@ -50,6 +50,8 @@ When you're ready, click the **Create page** and the new page will be created. ...@@ -50,6 +50,8 @@ When you're ready, click the **Create page** and the new page will be created.
![New page](img/wiki_create_new_page.png) ![New page](img/wiki_create_new_page.png)
### Attachment storage
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33475) in GitLab 11.3. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33475) in GitLab 11.3.
Starting with GitLab 11.3, any file that is uploaded to the wiki via GitLab's Starting with GitLab 11.3, any file that is uploaded to the wiki via GitLab's
...@@ -58,6 +60,22 @@ if you clone the wiki repository locally. All uploaded files prior to GitLab ...@@ -58,6 +60,22 @@ if you clone the wiki repository locally. All uploaded files prior to GitLab
11.3 are stored in GitLab itself. If you want them to be part of the wiki's Git 11.3 are stored in GitLab itself. If you want them to be part of the wiki's Git
repository, you will have to upload them again. repository, you will have to upload them again.
### Length restrictions for file and directory names
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24364) in GitLab 12.8.
Many common file systems have a [limit of 255 bytes for file and directory names](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits), and while Git and GitLab both support paths exceeding those limits, the presence of them makes it impossible for users on those file systems to checkout a wiki repository locally.
To avoid this situation, these limits are enforced when editing pages through the GitLab web interface and API:
- 245 bytes for page titles (reserving 10 bytes for the file extension).
- 255 bytes for directory names.
Please note that:
- Non-ASCII characters take up more than one byte.
- It's still possible to create files and directories exceeding those limits locally through Git, but this might break on other people's machines.
## Editing a wiki page ## Editing a wiki page
NOTE: **Note:** NOTE: **Note:**
......
...@@ -59,7 +59,7 @@ module API ...@@ -59,7 +59,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes' requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Group' optional :namespace, type: String, desc: 'Unique namespace related to Group'
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end end
use :create_params_ee use :create_params_ee
end end
...@@ -96,7 +96,7 @@ module API ...@@ -96,7 +96,7 @@ module API
put ':id/clusters/:cluster_id' do put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster authorize! :update_cluster, cluster
update_service = Clusters::UpdateService.new(current_user, update_cluster_params) update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster) if update_service.execute(cluster)
present cluster, with: Entities::ClusterGroup present cluster, with: Entities::ClusterGroup
......
...@@ -62,7 +62,7 @@ module API ...@@ -62,7 +62,7 @@ module API
requires :token, type: String, desc: 'Token to authenticate against Kubernetes' requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)' optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project' optional :namespace, type: String, desc: 'Unique namespace related to Project'
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC' optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end end
use :create_params_ee use :create_params_ee
end end
...@@ -100,7 +100,7 @@ module API ...@@ -100,7 +100,7 @@ module API
put ':id/clusters/:cluster_id' do put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster authorize! :update_cluster, cluster
update_service = Clusters::UpdateService.new(current_user, update_cluster_params) update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
if update_service.execute(cluster) if update_service.execute(cluster)
present cluster, with: Entities::ClusterProject present cluster, with: Entities::ClusterProject
......
...@@ -10,14 +10,14 @@ module Gitlab ...@@ -10,14 +10,14 @@ module Gitlab
end end
def match(content) def match(content)
content.match %r{^/#{all_names.join('|')} ?(.*)$} content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$}
end end
def perform_substitution(context, content) def perform_substitution(context, content)
return unless content return unless content
all_names.each do |a_name| all_names.each do |a_name|
content = content.gsub(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1')) content = content.gsub(%r{/#{a_name}(?![\S]) ?(.*)$}i, execute_block(action_block, context, '\1'))
end end
content content
......
...@@ -472,9 +472,6 @@ msgstr "" ...@@ -472,9 +472,6 @@ msgstr ""
msgid "%{total} open issues" msgid "%{total} open issues"
msgstr "" msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr "" msgstr ""
...@@ -733,9 +730,6 @@ msgstr "" ...@@ -733,9 +730,6 @@ msgstr ""
msgid "<no scopes selected>" msgid "<no scopes selected>"
msgstr "" msgstr ""
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
msgstr ""
msgid "<strong>%{group_name}</strong> group members" msgid "<strong>%{group_name}</strong> group members"
msgstr "" msgstr ""
...@@ -1676,9 +1670,6 @@ msgstr "" ...@@ -1676,9 +1670,6 @@ msgstr ""
msgid "An error occurred fetching the dropdown data." msgid "An error occurred fetching the dropdown data."
msgstr "" msgstr ""
msgid "An error occurred loading code navigation"
msgstr ""
msgid "An error occurred previewing the blob" msgid "An error occurred previewing the blob"
msgstr "" msgstr ""
...@@ -5071,6 +5062,9 @@ msgstr "" ...@@ -5071,6 +5062,9 @@ msgstr ""
msgid "Container Scanning" msgid "Container Scanning"
msgstr "" msgstr ""
msgid "Container does not exist"
msgstr ""
msgid "Container registry images" msgid "Container registry images"
msgstr "" msgstr ""
...@@ -5080,6 +5074,9 @@ msgstr "" ...@@ -5080,6 +5074,9 @@ msgstr ""
msgid "Container repositories sync capacity" msgid "Container repositories sync capacity"
msgstr "" msgstr ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept." msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "" msgstr ""
...@@ -5131,6 +5128,9 @@ msgstr "" ...@@ -5131,6 +5128,9 @@ msgstr ""
msgid "ContainerRegistry|Last Updated" msgid "ContainerRegistry|Last Updated"
msgstr "" msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr ""
msgid "ContainerRegistry|Number of tags to retain:" msgid "ContainerRegistry|Number of tags to retain:"
msgstr "" msgstr ""
...@@ -5196,12 +5196,21 @@ msgstr "" ...@@ -5196,12 +5196,21 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
msgstr ""
msgid "ContainerRegistry|You are about to remove %{item}. Are you sure?"
msgstr ""
msgid "ContainerRegistry|You are about to remove <b>%{count}</b> tags. Are you sure?" msgid "ContainerRegistry|You are about to remove <b>%{count}</b> tags. Are you sure?"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove <b>%{title}</b>. Are you sure?" msgid "ContainerRegistry|You are about to remove <b>%{title}</b>. Are you sure?"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted."
msgstr ""
msgid "ContainerRegistry|You are about to remove repository <b>%{title}</b>. Once you confirm, this repository will be permanently deleted." msgid "ContainerRegistry|You are about to remove repository <b>%{title}</b>. Once you confirm, this repository will be permanently deleted."
msgstr "" msgstr ""
...@@ -12674,6 +12683,9 @@ msgstr "" ...@@ -12674,6 +12683,9 @@ msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!" msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "" msgstr ""
msgid "No containers available"
msgstr ""
msgid "No contributions" msgid "No contributions"
msgstr "" msgstr ""
...@@ -14020,6 +14032,9 @@ msgstr "" ...@@ -14020,6 +14032,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will." msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "" msgstr ""
msgid "Pod does not exist"
msgstr ""
msgid "Pod logs" msgid "Pod logs"
msgstr "" msgstr ""
...@@ -20425,6 +20440,9 @@ msgstr "" ...@@ -20425,6 +20440,9 @@ msgstr ""
msgid "Unable to collect memory info" msgid "Unable to collect memory info"
msgstr "" msgstr ""
msgid "Unable to connect to Elasticsearch"
msgstr ""
msgid "Unable to connect to Prometheus server" msgid "Unable to connect to Prometheus server"
msgstr "" msgstr ""
...@@ -20494,6 +20512,9 @@ msgstr "" ...@@ -20494,6 +20512,9 @@ msgstr ""
msgid "Unknown Error" msgid "Unknown Error"
msgstr "" msgstr ""
msgid "Unknown cache key"
msgstr ""
msgid "Unknown encryption strategy: %{encrypted_strategy}!" msgid "Unknown encryption strategy: %{encrypted_strategy}!"
msgstr "" msgstr ""
...@@ -22766,6 +22787,12 @@ msgstr "" ...@@ -22766,6 +22787,12 @@ msgstr ""
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command." msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr "" msgstr ""
msgid "exceeds the limit of %{bytes} bytes for directory names"
msgstr ""
msgid "exceeds the limit of %{bytes} bytes for page titles"
msgstr ""
msgid "expired on %{milestone_due_date}" msgid "expired on %{milestone_due_date}"
msgstr "" msgstr ""
......
...@@ -2,35 +2,34 @@ ...@@ -2,35 +2,34 @@
module QA module QA
context 'Plan' do context 'Plan' do
describe 'Close issue' do include Support::Api
describe 'Issue' do
let(:issue) do let(:issue) do
Resource::Issue.fabricate_via_api! Resource::Issue.fabricate_via_api!
end end
let(:issue_id) { issue.api_response[:iid] } let(:issue_id) { issue.api_response[:iid] }
before do let(:api_client) { Runtime::API::Client.new(:gitlab) }
Flow::Login.sign_in
before do
# Initial commit should be pushed because # Initial commit should be pushed because
# the very first commit to the project doesn't close the issue # the very first commit to the project doesn't close the issue
# https://gitlab.com/gitlab-org/gitlab-foss/issues/38965 # https://gitlab.com/gitlab-org/gitlab-foss/issues/38965
push_commit('Initial commit') push_commit('Initial commit')
end end
it 'closes an issue by pushing a commit' do it 'closes via pushing a commit' do
push_commit("Closes ##{issue_id}", false) push_commit("Closes ##{issue_id}", false)
issue.visit! Support::Retrier.retry_until(max_duration: 10, sleep_interval: 1) do
issue_closed?
Page::Project::Issue::Show.perform do |show|
reopen_issue_button_visible = show.wait_until(reload: true) do
show.has_element?(:reopen_issue_button, wait: 1.0)
end
expect(reopen_issue_button_visible).to be_truthy
end end
end end
private
def push_commit(commit_message, new_branch = true) def push_commit(commit_message, new_branch = true)
Resource::Repository::ProjectPush.fabricate! do |push| Resource::Repository::ProjectPush.fabricate! do |push|
push.commit_message = commit_message push.commit_message = commit_message
...@@ -39,6 +38,11 @@ module QA ...@@ -39,6 +38,11 @@ module QA
push.project = issue.project push.project = issue.project
end end
end end
def issue_closed?
response = get Runtime::API::Request.new(api_client, "/projects/#{issue.project.id}/issues/#{issue_id}").url
parse_body(response)[:state] == 'closed'
end
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
context 'Plan', :smoke, :reliable do context 'Plan', :smoke do
describe 'Issue creation' do describe 'Issue creation' do
before do before do
Flow::Login.sign_in Flow::Login.sign_in
end end
it 'creates an issue' do it 'creates an issue', :reliable do
issue = Resource::Issue.fabricate_via_browser_ui! issue = Resource::Issue.fabricate_via_browser_ui!
Page::Project::Menu.perform(&:click_issues) Page::Project::Menu.perform(&:click_issues)
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions'; import actions from '~/code_navigation/store/actions';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils'; import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
jest.mock('~/flash');
jest.mock('~/code_navigation/utils'); jest.mock('~/code_navigation/utils');
describe('Code navigation actions', () => { describe('Code navigation actions', () => {
...@@ -25,13 +23,6 @@ describe('Code navigation actions', () => { ...@@ -25,13 +23,6 @@ describe('Code navigation actions', () => {
describe('requestDataError', () => { describe('requestDataError', () => {
it('commits REQUEST_DATA_ERROR', () => it('commits REQUEST_DATA_ERROR', () =>
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], [])); testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []));
it('creates a flash message', () =>
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then(
() => {
expect(createFlash).toHaveBeenCalled();
},
));
}); });
describe('fetchData', () => { describe('fetchData', () => {
......
...@@ -534,27 +534,21 @@ describe('IDE store file actions', () => { ...@@ -534,27 +534,21 @@ describe('IDE store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('adds a newline to the end of the file if it doesnt already exist', done => { it('adds file into stagedFiles array', done => {
callAction('content') store
.then(() => { .dispatch('changeFileContent', {
expect(tmpFile.content).toBe('content\n'); path: tmpFile.path,
content: 'content',
done();
}) })
.catch(done.fail);
});
it('adds file into changedFiles array', done => {
callAction()
.then(() => { .then(() => {
expect(store.state.changedFiles.length).toBe(1); expect(store.state.stagedFiles.length).toBe(1);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('adds file not more than once into changedFiles array', done => { it('adds file not more than once into stagedFiles array', done => {
store store
.dispatch('changeFileContent', { .dispatch('changeFileContent', {
path: tmpFile.path, path: tmpFile.path,
...@@ -567,7 +561,7 @@ describe('IDE store file actions', () => { ...@@ -567,7 +561,7 @@ describe('IDE store file actions', () => {
}), }),
) )
.then(() => { .then(() => {
expect(store.state.changedFiles.length).toBe(1); expect(store.state.stagedFiles.length).toBe(1);
done(); done();
}) })
...@@ -594,52 +588,6 @@ describe('IDE store file actions', () => { ...@@ -594,52 +588,6 @@ describe('IDE store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('adds file into stagedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() => {
expect(store.state.stagedFiles.length).toBe(1);
done();
})
.catch(done.fail);
});
it('adds file not more than once into stagedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() =>
store.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content 123',
}),
)
.then(() => {
expect(store.state.stagedFiles.length).toBe(1);
done();
})
.catch(done.fail);
});
});
it('bursts unused seal', done => { it('bursts unused seal', done => {
store store
.dispatch('changeFileContent', { .dispatch('changeFileContent', {
......
...@@ -61,19 +61,14 @@ describe('IDE store integration', () => { ...@@ -61,19 +61,14 @@ describe('IDE store integration', () => {
store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' }); store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' });
}); });
it('has changed and staged', () => { it('is added to staged as modified', () => {
expect(store.state.changedFiles).toEqual([
expect.objectContaining({
path: TEST_PATH,
tempFile: true,
deleted: false,
}),
]);
expect(store.state.stagedFiles).toEqual([ expect(store.state.stagedFiles).toEqual([
expect.objectContaining({ expect.objectContaining({
path: TEST_PATH, path: TEST_PATH,
deleted: true, deleted: false,
staged: true,
changed: true,
tempFile: false,
}), }),
]); ]);
}); });
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Group Empty state to match the default snapshot 1`] = `
<div
class="container-message"
svg-path="foo"
title="There are no container images available in this group"
>
<p
class="js-no-container-images-text"
>
With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
<gl-link-stub
href="baz"
target="_blank"
>
More Information
</gl-link-stub>
</p>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry Project Empty state to match the default snapshot 1`] = `
<div
class="container-message"
svg-path="bazFoo"
title="There are no container images stored for this project"
>
<p
class="js-no-container-images-text"
>
With the Container Registry, every project can have its own space to store its Docker images.
<gl-link-stub
href="baz"
target="_blank"
>
More Information
</gl-link-stub>
</p>
<h5>
Quick Start
</h5>
<p
class="js-not-logged-in-to-registry-text"
>
If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
<gl-link-stub
href="barBaz"
target="_blank"
>
Two-Factor Authentication
</gl-link-stub>
enabled, use a
<gl-link-stub
href="fooBaz"
target="_blank"
>
Personal Access Token
</gl-link-stub>
instead of a password.
</p>
<div
class="input-group append-bottom-10"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker login bar"
title="Copy login command"
tooltipplacement="top"
/>
</span>
</div>
<p />
<p>
You can add an image to this registry with the following commands:
</p>
<div
class="input-group append-bottom-10"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker build -t foo ."
title="Copy build command"
tooltipplacement="top"
/>
</span>
</div>
<div
class="input-group"
>
<input
class="form-control monospace"
readonly="readonly"
type="text"
/>
<span
class="input-group-append"
>
<clipboard-button-stub
class="input-group-text"
cssclass="btn-default"
text="docker push foo"
title="Copy push command"
tooltipplacement="top"
/>
</span>
</div>
</div>
`;
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs';
import groupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Group Empty state', () => {
let wrapper;
let store;
beforeEach(() => {
store = new Vuex.Store({
state: {
config: {
noContainersImage: 'foo',
helpPagePath: 'baz',
},
},
});
wrapper = shallowMount(groupEmptyState, {
localVue,
store,
stubs: {
GlEmptyState,
GlSprintf,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs';
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry Project Empty state', () => {
let wrapper;
let store;
beforeEach(() => {
store = new Vuex.Store({
state: {
config: {
repositoryUrl: 'foo',
registryHostUrlWithPort: 'bar',
helpPagePath: 'baz',
twoFactorAuthHelpLink: 'barBaz',
personalAccessTokensHelpLink: 'fooBaz',
noContainersImage: 'bazFoo',
},
},
});
wrapper = shallowMount(projectEmptyState, {
localVue,
store,
stubs: {
GlEmptyState,
GlSprintf,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
export const headers = {
'X-PER-PAGE': 5,
'X-PAGE': 1,
'X-TOTAL': 13,
'X-TOTAL_PAGES': 1,
'X-NEXT-PAGE': null,
'X-PREVIOUS-PAGE': null,
};
export const reposServerResponse = [ export const reposServerResponse = [
{ {
destroy_path: 'path', destroy_path: 'path',
...@@ -36,3 +44,46 @@ export const registryServerResponse = [ ...@@ -36,3 +44,46 @@ export const registryServerResponse = [
created_at: 1505828744434, created_at: 1505828744434,
}, },
]; ];
export const imagesListResponse = {
data: [
{
path: 'foo',
location: 'location',
destroy_path: 'path',
},
{
path: 'bar',
location: 'location-2',
destroy_path: 'path-2',
},
],
headers,
};
export const tagsListResponse = {
data: [
{
tag: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
short_revision: 'b118ab5b0',
size: 19,
layers: 10,
location: 'location',
path: 'bar',
created_at: 1505828744434,
destroy_path: 'path',
},
{
tag: 'test-image',
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
short_revision: 'b969de599',
size: 19,
layers: 10,
path: 'foo',
location: 'location-2',
created_at: 1505828744434,
},
],
headers,
};
This diff is collapsed.
import VueRouter from 'vue-router';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { imagesListResponse } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('List Page', () => {
let wrapper;
let dispatchSpy;
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
const findEmptyState = () => wrapper.find(GlEmptyState);
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
const findPagination = () => wrapper.find(GlPagination);
beforeEach(() => {
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlModal,
GlEmptyState,
GlSprintf,
},
});
dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveImagesListSuccess', imagesListResponse);
});
afterEach(() => {
wrapper.destroy();
});
describe('connection error', () => {
const config = {
characterError: true,
containersErrorImage: 'foo',
helpPagePath: 'bar',
};
beforeAll(() => {
store.dispatch('setInitialState', config);
});
afterAll(() => {
store.dispatch('setInitialState', {});
});
it('should show an empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('empty state should have an svg-path', () => {
expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
});
it('empty state should have a description', () => {
expect(findEmptyState().html()).toContain('connection error');
});
it('should not show the loading or default state', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false);
});
});
describe('when isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('shows the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false);
});
});
describe('list', () => {
describe('listElement', () => {
let listElements;
let firstElement;
beforeEach(() => {
listElements = findRowItems();
[firstElement] = store.state.images;
});
it('contains one list element for each image', () => {
expect(listElements.length).toBe(store.state.images.length);
});
it('contains a link to the details page', () => {
const link = findDetailsLink();
expect(link.html()).toContain(firstElement.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(firstElement.location);
expect(button.props('title')).toBe(firstElement.location);
});
describe('delete image', () => {
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn();
expect(deleteBtn.exists()).toBe(true);
});
it('should call deleteItem when confirming deletion', () => {
dispatchSpy.mockResolvedValue();
const itemToDelete = wrapper.vm.images[0];
wrapper.setData({ itemToDelete });
findDeleteModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
itemToDelete.destroy_path,
);
});
});
});
describe('pagination', () => {
it('exists', () => {
expect(findPagination().exists()).toBe(true);
});
it('is wired to the correct pagination props', () => {
const pagination = findPagination();
expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
expect(pagination.props('value')).toBe(store.state.pagination.page);
});
it('fetch the data from the API when the v-model changes', () => {
dispatchSpy.mockReturnValue();
wrapper.setData({ currentPage: 2 });
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
});
});
});
});
describe('modal', () => {
it('exists', () => {
expect(findDeleteModal().exists()).toBe(true);
});
it('contains a description with the path of the item to delete', () => {
wrapper.setData({ itemToDelete: { path: 'foo' } });
return wrapper.vm.$nextTick().then(() => {
expect(findDeleteModal().html()).toContain('foo');
});
});
});
describe('tracking', () => {
const testTrackingCall = action => {
expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
label: 'registry_repository_delete',
});
};
beforeEach(() => {
jest.spyOn(Tracking, 'event');
dispatchSpy.mockReturnValue();
});
it('send an event when delete button is clicked', () => {
const deleteBtn = findDeleteBtn();
deleteBtn.vm.$emit('click');
testTrackingCall('click_button');
});
it('send an event when cancel is pressed on modal', () => {
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('cancel');
testTrackingCall('cancel_delete');
});
it('send an event when confirm is clicked on modal', () => {
dispatchSpy.mockReturnValue();
const deleteModal = findDeleteModal();
deleteModal.vm.$emit('ok');
testTrackingCall('confirm_delete');
});
});
});
});
...@@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => {
}); });
describe('fetch tags list', () => { describe('fetch tags list', () => {
const url = window.btoa(`${endpoint}/1}`); const url = `${endpoint}/1}`;
const path = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` }));
it('sets the tagsList', done => { it('sets the tagsList', done => {
mock.onGet(window.atob(url)).replyOnce(200, registryServerResponse, {}); mock.onGet(url).replyOnce(200, registryServerResponse, {});
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: url }, { id: path },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
...@@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
it('should create flash on error', done => { it('should create flash on error', done => {
testAction( testAction(
actions.requestTagsList, actions.requestTagsList,
{ id: url }, { id: path },
{}, {},
[ [
{ type: types.SET_MAIN_LOADING, payload: true }, { type: types.SET_MAIN_LOADING, payload: true },
......
...@@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const expectedState = { ...mockState, config: { endpoint: 'foo' } }; const payload = { endpoint: 'foo', isGroupPage: true };
mutations[types.SET_INITIAL_STATE](mockState, { endpoint: 'foo' }); const expectedState = { ...mockState, config: payload };
mutations[types.SET_INITIAL_STATE](mockState, payload);
expect(mockState).toEqual(expectedState); expect(mockState).toEqual(expectedState);
}); });
......
export const GlModal = {
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
methods: {
show: jest.fn(),
},
};
export const GlEmptyState = {
template: '<div><slot name="description"></slot></div>',
name: 'GlEmptyStateSTub',
};
...@@ -40,7 +40,7 @@ describe('Flash', () => { ...@@ -40,7 +40,7 @@ describe('Flash', () => {
expect(el.style['transition-property']).toBe('opacity'); expect(el.style['transition-property']).toBe('opacity');
expect(el.style['transition-duration']).toBe('0.3s'); expect(el.style['transition-duration']).toBe('0.15s');
}); });
it('sets opacity style', () => { it('sets opacity style', () => {
......
...@@ -52,7 +52,7 @@ describe('IDE commit form', () => { ...@@ -52,7 +52,7 @@ describe('IDE commit form', () => {
vm.$store.state.stagedFiles.push('test'); vm.$store.state.stagedFiles.push('test');
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes'); expect(vm.$el.querySelector('p').textContent).toContain('1 staged and 1 unstaged changes');
done(); done();
}); });
}); });
......
...@@ -63,7 +63,7 @@ describe('IDE extra file row component', () => { ...@@ -63,7 +63,7 @@ describe('IDE extra file row component', () => {
stagedFilesCount = 1; stagedFilesCount = 1;
unstagedFilesCount = 1; unstagedFilesCount = 1;
expect(vm.folderChangesTooltip).toBe('1 unstaged and 1 staged changes'); expect(vm.folderChangesTooltip).toBe('1 staged and 1 unstaged changes');
}); });
}); });
......
...@@ -225,35 +225,6 @@ describe('Multi-file store actions', () => { ...@@ -225,35 +225,6 @@ describe('Multi-file store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('adds tmp file to staged files', done => {
const name = 'test';
store
.dispatch('createTempEntry', {
name,
branchId: 'mybranch',
type: 'blob',
})
.then(() => {
expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]);
done();
})
.catch(done.fail);
});
});
it('adds tmp file to open files', done => { it('adds tmp file to open files', done => {
const name = 'test'; const name = 'test';
...@@ -274,7 +245,7 @@ describe('Multi-file store actions', () => { ...@@ -274,7 +245,7 @@ describe('Multi-file store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('adds tmp file to changed files', done => { it('adds tmp file to staged files', done => {
const name = 'test'; const name = 'test';
store store
...@@ -284,9 +255,7 @@ describe('Multi-file store actions', () => { ...@@ -284,9 +255,7 @@ describe('Multi-file store actions', () => {
type: 'blob', type: 'blob',
}) })
.then(() => { .then(() => {
expect(store.state.changedFiles).toEqual([ expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]);
jasmine.objectContaining({ name, tempFile: true }),
]);
done(); done();
}) })
...@@ -294,15 +263,9 @@ describe('Multi-file store actions', () => { ...@@ -294,15 +263,9 @@ describe('Multi-file store actions', () => {
}); });
it('sets tmp file as active', () => { it('sets tmp file as active', () => {
const dispatch = jasmine.createSpy(); createTempEntry(store, { name: 'test', branchId: 'mybranch', type: 'blob' });
const commit = jasmine.createSpy();
createTempEntry( expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
{ state: store.state, getters: store.getters, dispatch, commit },
{ name: 'test', branchId: 'mybranch', type: 'blob' },
);
expect(dispatch).toHaveBeenCalledWith('setFileActive', 'test');
}); });
it('creates flash message if file already exists', done => { it('creates flash message if file already exists', done => {
...@@ -804,17 +767,6 @@ describe('Multi-file store actions', () => { ...@@ -804,17 +767,6 @@ describe('Multi-file store actions', () => {
}); });
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('by default renames an entry and stages it', () => { it('by default renames an entry and stages it', () => {
const dispatch = jasmine.createSpy(); const dispatch = jasmine.createSpy();
const commit = jasmine.createSpy(); const commit = jasmine.createSpy();
...@@ -829,31 +781,6 @@ describe('Multi-file store actions', () => { ...@@ -829,31 +781,6 @@ describe('Multi-file store actions', () => {
[types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })], [types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })],
]); ]);
}); });
});
it('by default renames an entry and adds to changed', done => {
testAction(
renameEntry,
{ path: 'orig', name: 'renamed' },
store.state,
[
{
type: types.RENAME_ENTRY,
payload: {
path: 'orig',
name: 'renamed',
parentPath: undefined,
},
},
{
type: types.ADD_FILE_TO_CHANGED,
payload: 'renamed',
},
],
jasmine.any(Object),
done,
);
});
it('if not changed, completely unstages and discards entry if renamed to original', done => { it('if not changed, completely unstages and discards entry if renamed to original', done => {
testAction( testAction(
......
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore'; import { head } from 'lodash';
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui'; import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
...@@ -99,9 +99,9 @@ describe('ProjectSelector component', () => { ...@@ -99,9 +99,9 @@ describe('ProjectSelector component', () => {
it(`triggers a "projectClicked" event when a project is clicked`, () => { it(`triggers a "projectClicked" event when a project is clicked`, () => {
spyOn(vm, '$emit'); spyOn(vm, '$emit');
wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults)); wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults));
expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults)); expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
}); });
it(`shows a "no results" message if showNoResultsMessage === true`, () => { it(`shows a "no results" message if showNoResultsMessage === true`, () => {
......
...@@ -19,6 +19,7 @@ EOF ...@@ -19,6 +19,7 @@ EOF
expect(subject.perform_substitution(self, nil)).to be_nil expect(subject.perform_substitution(self, nil)).to be_nil
end end
context 'when content contains command name' do
it 'performs the substitution by default' do it 'performs the substitution by default' do
expect(subject.perform_substitution(self, content)).to eq <<EOF expect(subject.perform_substitution(self, content)).to eq <<EOF
Hello! Let's do this! Hello! Let's do this!
...@@ -27,6 +28,23 @@ EOF ...@@ -27,6 +28,23 @@ EOF
end end
end end
context 'when content contains command name in word' do
let(:content) do
<<EOF
Hello! Let's do this!
`/sub_names` I like this stuff
EOF
end
it 'does not perform the substitution' do
expect(subject.perform_substitution(self, content)).to eq <<EOF
Hello! Let's do this!
`/sub_names` I like this stuff
EOF
end
end
end
describe '#match' do describe '#match' do
it 'checks the content for the command' do it 'checks the content for the command' do
expect(subject.match(content)).to be_truthy expect(subject.match(content)).to be_truthy
...@@ -41,5 +59,9 @@ EOF ...@@ -41,5 +59,9 @@ EOF
it 'is nil if content does not have the command' do it 'is nil if content does not have the command' do
expect(subject.match('blah')).to be_falsey expect(subject.match('blah')).to be_falsey
end end
it 'is nil if content contains the command as prefix' do
expect(subject.match('/sub_namex')).to be_falsey
end
end end
end end
...@@ -33,7 +33,7 @@ describe Ci::Build do ...@@ -33,7 +33,7 @@ describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) } it { is_expected.to respond_to(:trace) }
it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
......
...@@ -162,6 +162,23 @@ describe Ci::Pipeline, :mailer do ...@@ -162,6 +162,23 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#merge_request?' do
let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
let(:merge_request) { create(:merge_request) }
it 'returns true' do
expect(pipeline).to be_merge_request
end
context 'when merge request is nil' do
let(:merge_request) { nil }
it 'returns false' do
expect(pipeline).not_to be_merge_request
end
end
end
describe '#detached_merge_request_pipeline?' do describe '#detached_merge_request_pipeline?' do
subject { pipeline.detached_merge_request_pipeline? } subject { pipeline.detached_merge_request_pipeline? }
...@@ -367,48 +384,6 @@ describe Ci::Pipeline, :mailer do ...@@ -367,48 +384,6 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe 'Validations for merge request pipelines' do
let(:pipeline) do
build(:ci_pipeline, source: source, merge_request: merge_request)
end
let(:merge_request) do
create(:merge_request,
source_project: project,
source_branch: 'feature',
target_project: project,
target_branch: 'master')
end
context 'when source is merge request' do
let(:source) { :merge_request_event }
context 'when merge request is specified' do
it { expect(pipeline).to be_valid }
end
context 'when merge request is empty' do
let(:merge_request) { nil }
it { expect(pipeline).not_to be_valid }
end
end
context 'when source is web' do
let(:source) { :web }
context 'when merge request is specified' do
it { expect(pipeline).not_to be_valid }
end
context 'when merge request is empty' do
let(:merge_request) { nil }
it { expect(pipeline).to be_valid }
end
end
end
describe 'modules' do describe 'modules' do
it_behaves_like 'AtomicInternalId', validate_presence: false do it_behaves_like 'AtomicInternalId', validate_presence: false do
let(:internal_id_attribute) { :iid } let(:internal_id_attribute) { :iid }
...@@ -612,9 +587,9 @@ describe Ci::Pipeline, :mailer do ...@@ -612,9 +587,9 @@ describe Ci::Pipeline, :mailer do
] ]
end end
context 'when source is merge request' do context 'when pipeline is merge request' do
let(:pipeline) do let(:pipeline) do
create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) create(:ci_pipeline, merge_request: merge_request)
end end
let(:merge_request) do let(:merge_request) do
...@@ -651,7 +626,7 @@ describe Ci::Pipeline, :mailer do ...@@ -651,7 +626,7 @@ describe Ci::Pipeline, :mailer do
'CI_MERGE_REQUEST_TITLE' => merge_request.title, 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','), 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
'CI_MERGE_REQUEST_EVENT_TYPE' => pipeline.merge_request_event_type.to_s) 'CI_MERGE_REQUEST_EVENT_TYPE' => pipeline.merge_request_event_type.to_s)
end end
...@@ -1263,9 +1238,9 @@ describe Ci::Pipeline, :mailer do ...@@ -1263,9 +1238,9 @@ describe Ci::Pipeline, :mailer do
is_expected.to be_truthy is_expected.to be_truthy
end end
context 'when source is merge request' do context 'when pipeline is merge request' do
let(:pipeline) do let(:pipeline) do
create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) create(:ci_pipeline, merge_request: merge_request)
end end
let(:merge_request) do let(:merge_request) do
......
This diff is collapsed.
...@@ -148,6 +148,12 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -148,6 +148,12 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
context 'when "include" is provided' do context 'when "include" is provided' do
let(:file_content) do
YAML.dump(
rspec: { script: 'rspec' },
echo: { script: 'echo' })
end
shared_examples 'creates a child pipeline' do shared_examples 'creates a child pipeline' do
it 'creates only one new pipeline' do it 'creates only one new pipeline' do
expect { service.execute(bridge) } expect { service.execute(bridge) }
...@@ -189,9 +195,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -189,9 +195,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end end
before do before do
file_content = YAML.dump(
rspec: { script: 'rspec' },
echo: { script: 'echo' })
upstream_project.repository.create_file( upstream_project.repository.create_file(
user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master') user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master')
...@@ -218,6 +221,29 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do ...@@ -218,6 +221,29 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
it_behaves_like 'creates a child pipeline' it_behaves_like 'creates a child pipeline'
end end
context 'when the parent is a merge request pipeline' do
let(:merge_request) { create(:merge_request, source_project: bridge.project, target_project: bridge.project) }
let(:file_content) do
YAML.dump(
workflow: { rules: [{ if: '$CI_MERGE_REQUEST_ID' }] },
rspec: { script: 'rspec' },
echo: { script: 'echo' })
end
before do
bridge.pipeline.update!(source: :merge_request_event, merge_request: merge_request)
end
it_behaves_like 'creates a child pipeline'
it 'propagates the merge request to the child pipeline' do
pipeline = service.execute(bridge)
expect(pipeline.merge_request).to eq(merge_request)
expect(pipeline).to be_merge_request
end
end
context 'when upstream pipeline is a child pipeline' do context 'when upstream pipeline is a child pipeline' do
let!(:pipeline_source) do let!(:pipeline_source) do
create(:ci_sources_pipeline, create(:ci_sources_pipeline,
......
...@@ -1473,15 +1473,6 @@ describe Ci::CreatePipelineService do ...@@ -1473,15 +1473,6 @@ describe Ci::CreatePipelineService do
end end
end end
end end
context 'when merge request is not specified' do
let(:merge_request) { nil }
it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:merge_request]).to eq(["can't be blank"])
end
end
end end
context "when config does not have merge_requests keywords" do context "when config does not have merge_requests keywords" do
...@@ -1518,17 +1509,6 @@ describe Ci::CreatePipelineService do ...@@ -1518,17 +1509,6 @@ describe Ci::CreatePipelineService do
.to eq(['No stages / jobs for this pipeline.']) .to eq(['No stages / jobs for this pipeline.'])
end end
end end
context 'when merge request is not specified' do
let(:merge_request) { nil }
it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
.to eq(['No stages / jobs for this pipeline.'])
end
end
end end
context "when config uses regular expression for only keyword" do context "when config uses regular expression for only keyword" do
...@@ -1623,6 +1603,7 @@ describe Ci::CreatePipelineService do ...@@ -1623,6 +1603,7 @@ describe Ci::CreatePipelineService do
context 'when source is web' do context 'when source is web' do
let(:source) { :web } let(:source) { :web }
let(:merge_request) { nil }
context "when config has merge_requests keywords" do context "when config has merge_requests keywords" do
let(:config) do let(:config) do
...@@ -1644,24 +1625,6 @@ describe Ci::CreatePipelineService do ...@@ -1644,24 +1625,6 @@ describe Ci::CreatePipelineService do
} }
end end
context 'when merge request is specified' do
let(:merge_request) do
create(:merge_request,
source_project: project,
source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
it 'does not create a merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:merge_request]).to eq(["must be blank"])
end
end
context 'when merge request is not specified' do
let(:merge_request) { nil }
it 'creates a branch pipeline' do it 'creates a branch pipeline' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(pipeline).to be_web expect(pipeline).to be_web
...@@ -1671,7 +1634,6 @@ describe Ci::CreatePipelineService do ...@@ -1671,7 +1634,6 @@ describe Ci::CreatePipelineService do
end end
end end
end end
end
context 'when needs is used' do context 'when needs is used' do
let(:pipeline) { execute_service } let(:pipeline) { execute_service }
......
...@@ -101,7 +101,7 @@ module KubernetesHelpers ...@@ -101,7 +101,7 @@ module KubernetesHelpers
end end
logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \ logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \
"/log?#{container_query_param}tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}&timestamps=true" "/log?#{container_query_param}tailLines=#{::PodLogs::KubernetesService::LOGS_LIMIT}&timestamps=true"
if status if status
response = { status: status } response = { status: status }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment