diff options
147 files changed, 2384 insertions, 398 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 4149c39eec6..2bd6f7e3927 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -10.1.0 +10.2.0 diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 702f00888d0..77c68cac4a6 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -26,7 +26,7 @@ export default { if (log.append) { if (isNewJobLogActive()) { - state.trace = updateIncrementalTrace(log.lines, state.trace); + state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace; } else { state.trace += log.html; } @@ -35,9 +35,9 @@ export default { // When the job still does not have a trace // the trace response will not have a defined // html or size. We keep the old value otherwise these - // will be set to `undefined` + // will be set to `null` if (isNewJobLogActive()) { - state.trace = logLinesParser(log.lines) || state.trace; + state.trace = log.lines ? logLinesParser(log.lines) : state.trace; } else { state.trace = log.html || state.trace; } diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js index 12069e0c123..58e49f54d96 100644 --- a/app/assets/javascripts/jobs/store/utils.js +++ b/app/assets/javascripts/jobs/store/utils.js @@ -147,13 +147,15 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => { const firstNew = newLog[0]; - if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) { - cloneOldLog.splice(lastIndex); - } else if (last.lines && last.lines.length) { - const lastNestedIndex = last.lines.length - 1; - const lastNested = last.lines[lastNestedIndex]; - if (lastNested.offset === firstNew.offset) { - last.lines.splice(lastNestedIndex); + if (last && firstNew) { + if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) { + cloneOldLog.splice(lastIndex); + } else if (last.lines && last.lines.length) { + const lastNestedIndex = last.lines.length - 1; + const lastNested = last.lines[lastNestedIndex]; + if (lastNested.offset === firstNew.offset) { + last.lines.splice(lastNestedIndex); + } } } @@ -170,7 +172,7 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => { * @param array oldLog * @param array newLog */ -export const updateIncrementalTrace = (newLog, oldParsed = []) => { +export const updateIncrementalTrace = (newLog = [], oldParsed = []) => { const parsedLog = findOffsetAndRemove(newLog, oldParsed); return logLinesParser(newLog, parsedLog); diff --git a/app/assets/javascripts/pages/groups/registry/repositories/index.js b/app/assets/javascripts/pages/groups/registry/repositories/index.js new file mode 100644 index 00000000000..b663defad0e --- /dev/null +++ b/app/assets/javascripts/pages/groups/registry/repositories/index.js @@ -0,0 +1,3 @@ +import initRegistryImages from '~/registry'; + +document.addEventListener('DOMContentLoaded', initRegistryImages); diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index a20bae9e37e..11b2c3b7016 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -2,17 +2,19 @@ import { mapGetters, mapActions } from 'vuex'; import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import store from '../stores'; -import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import CollapsibleContainer from './collapsible_container.vue'; +import ProjectEmptyState from './project_empty_state.vue'; +import GroupEmptyState from './group_empty_state.vue'; import { s__, sprintf } from '../../locale'; export default { name: 'RegistryListApp', components: { - clipboardButton, CollapsibleContainer, GlEmptyState, GlLoadingIcon, + ProjectEmptyState, + GroupEmptyState, }, props: { characterError: { @@ -38,19 +40,27 @@ export default { }, personalAccessTokensHelpLink: { type: String, - required: true, + required: false, + default: null, }, registryHostUrlWithPort: { type: String, - required: true, + required: false, + default: null, }, repositoryUrl: { type: String, required: true, }, + isGroupPage: { + type: Boolean, + default: false, + required: false, + }, twoFactorAuthHelpLink: { type: String, - required: true, + required: false, + default: null, }, }, store, @@ -91,37 +101,10 @@ export default { false, ); }, - notLoggedInToRegistryText() { - return sprintf( - 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.`), - { - twofaDocLinkStart: `<a href="${this.twoFactorAuthHelpLink}" target="_blank">`, - twofaDocLinkEnd: '</a>', - personalAccessTokensDocLinkStart: `<a href="${this.personalAccessTokensHelpLink}" target="_blank">`, - personalAccessTokensDocLinkEnd: '</a>', - }, - false, - ); - }, - dockerLoginCommand() { - // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - return `docker login ${this.registryHostUrlWithPort}`; - }, - dockerBuildCommand() { - // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - return `docker build -t ${this.repositoryUrl} .`; - }, - dockerPushCommand() { - // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings - return `docker push ${this.repositoryUrl}`; - }, }, created() { this.setMainEndpoint(this.endpoint); + this.setIsDeleteDisabled(this.isGroupPage); }, mounted() { if (!this.characterError) { @@ -129,7 +112,7 @@ export default { } }, methods: { - ...mapActions(['setMainEndpoint', 'fetchRepos']), + ...mapActions(['setMainEndpoint', 'fetchRepos', 'setIsDeleteDisabled']), }, }; </script> @@ -152,57 +135,19 @@ export default { <p v-html="introText"></p> <collapsible-container v-for="item in repos" :key="item.id" :repo="item" /> </div> - - <gl-empty-state - v-else - :title="s__('ContainerRegistry|There are no container images stored for this project')" - :svg-path="noContainersImage" - class="container-message" - > - <template #description> - <p class="js-no-container-images-text" v-html="noContainerImagesText"></p> - <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> - <p class="js-not-logged-in-to-registry-text" v-html="notLoggedInToRegistryText"></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> - {{ - 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> + <project-empty-state + v-else-if="!isGroupPage" + :no-containers-image="noContainersImage" + :help-page-path="helpPagePath" + :repository-url="repositoryUrl" + :two-factor-auth-help-link="twoFactorAuthHelpLink" + :personal-access-tokens-help-link="personalAccessTokensHelpLink" + :registry-host-url-with-port="registryHostUrlWithPort" + /> + <group-empty-state + v-else-if="isGroupPage" + :no-containers-image="noContainersImage" + :help-page-path="helpPagePath" + /> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 3e31d24088e..ed48331f459 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import { GlLoadingIcon, GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui'; import createFlash from '../../flash'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; @@ -35,9 +35,13 @@ export default { }; }, computed: { + ...mapGetters(['isDeleteDisabled']), iconName() { return this.isOpen ? 'angle-up' : 'angle-right'; }, + canDeleteRepo() { + return this.repo.canDelete && !this.isDeleteDisabled; + }, }, methods: { ...mapActions(['fetchRepos', 'fetchList', 'deleteItem']), @@ -80,7 +84,7 @@ export default { <div class="controls d-none d-sm-block float-right"> <gl-button - v-if="repo.canDelete" + v-if="canDeleteRepo" v-gl-tooltip v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove repository')" @@ -98,7 +102,7 @@ export default { <gl-loading-icon v-if="repo.isLoading" size="md" class="append-bottom-20" /> <div v-else-if="!repo.isLoading && isOpen" class="container-image-tags"> - <table-registry v-if="repo.list.length" :repo="repo" /> + <table-registry v-if="repo.list.length" :repo="repo" :can-delete-repo="canDeleteRepo" /> <div v-else class="nothing-here-block"> {{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }} diff --git a/app/assets/javascripts/registry/components/group_empty_state.vue b/app/assets/javascripts/registry/components/group_empty_state.vue new file mode 100644 index 00000000000..7885fd2146d --- /dev/null +++ b/app/assets/javascripts/registry/components/group_empty_state.vue @@ -0,0 +1,46 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; + +export default { + name: 'GroupEmptyState', + components: { + GlEmptyState, + }, + props: { + noContainersImage: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + computed: { + noContainerImagesText() { + return sprintf( + 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}`, + ), + { + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, + docLinkEnd: '</a>', + }, + false, + ); + }, + }, +}; +</script> +<template> + <gl-empty-state + :title="s__('ContainerRegistry|There are no container images available in this group')" + :svg-path="noContainersImage" + class="container-message" + > + <template #description> + <p class="js-no-container-images-text" v-html="noContainerImagesText"></p> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/registry/components/project_empty_state.vue b/app/assets/javascripts/registry/components/project_empty_state.vue new file mode 100644 index 00000000000..80ef31004c8 --- /dev/null +++ b/app/assets/javascripts/registry/components/project_empty_state.vue @@ -0,0 +1,133 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { s__, sprintf } from '~/locale'; + +export default { + name: 'ProjectEmptyState', + components: { + ClipboardButton, + GlEmptyState, + }, + props: { + noContainersImage: { + type: String, + required: true, + }, + repositoryUrl: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + twoFactorAuthHelpLink: { + type: String, + required: true, + }, + personalAccessTokensHelpLink: { + type: String, + required: true, + }, + registryHostUrlWithPort: { + type: String, + required: true, + }, + }, + computed: { + dockerBuildCommand() { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + return `docker build -t ${this.repositoryUrl} .`; + }, + dockerPushCommand() { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + return `docker push ${this.repositoryUrl}`; + }, + dockerLoginCommand() { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + return `docker login ${this.registryHostUrlWithPort}`; + }, + noContainerImagesText() { + return sprintf( + s__(`ContainerRegistry|With the Container Registry, every project can have its own space to + store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`), + { + docLinkStart: `<a href="${this.helpPagePath}" target="_blank">`, + docLinkEnd: '</a>', + }, + false, + ); + }, + notLoggedInToRegistryText() { + return sprintf( + 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.`), + { + twofaDocLinkStart: `<a href="${this.twoFactorAuthHelpLink}" target="_blank">`, + twofaDocLinkEnd: '</a>', + personalAccessTokensDocLinkStart: `<a href="${this.personalAccessTokensHelpLink}" target="_blank">`, + personalAccessTokensDocLinkEnd: '</a>', + }, + false, + ); + }, + }, +}; +</script> +<template> + <gl-empty-state + :title="s__('ContainerRegistry|There are no container images stored for this project')" + :svg-path="noContainersImage" + class="container-message" + > + <template #description> + <p class="js-no-container-images-text" v-html="noContainerImagesText"></p> + <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> + <p class="js-not-logged-in-to-registry-text" v-html="notLoggedInToRegistryText"></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> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 00acc0eb04a..ac7272c4d29 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import { GlButton, GlFormCheckbox, @@ -35,6 +35,11 @@ export default { type: Object, required: true, }, + canDeleteRepo: { + type: Boolean, + default: false, + required: false, + }, }, data() { return { @@ -45,6 +50,7 @@ export default { }; }, computed: { + ...mapGetters(['isDeleteDisabled']), bulkDeletePath() { return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : ''; }, @@ -165,6 +171,9 @@ export default { } } }, + canDeleteRow(item) { + return item && item.canDelete && !this.isDeleteDisabled; + }, }, }; </script> @@ -175,7 +184,7 @@ export default { <tr> <th> <gl-form-checkbox - v-if="repo.canDelete" + v-if="canDeleteRepo" class="js-select-all-checkbox" :checked="selectAllChecked" @change="onSelectAllChange" @@ -187,7 +196,7 @@ export default { <th>{{ s__('ContainerRegistry|Last Updated') }}</th> <th> <gl-button - v-if="repo.canDelete" + v-if="canDeleteRepo" v-gl-tooltip v-gl-modal="modalId" :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0" @@ -208,7 +217,7 @@ export default { <tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row"> <td class="check"> <gl-form-checkbox - v-if="item.canDelete" + v-if="canDeleteRow(item)" class="js-select-checkbox" :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)" @change="updateItemsToBeDeleted(index)" @@ -244,7 +253,7 @@ export default { <td class="content action-buttons"> <gl-button - v-if="item.canDelete" + v-if="canDeleteRow(item)" v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index 38c3d67042c..18fd360f586 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -13,29 +13,24 @@ export default () => data() { const { dataset } = document.querySelector(this.$options.el); return { - characterError: Boolean(dataset.characterError), - containersErrorImage: dataset.containersErrorImage, - endpoint: dataset.endpoint, - helpPagePath: dataset.helpPagePath, - noContainersImage: dataset.noContainersImage, - personalAccessTokensHelpLink: dataset.personalAccessTokensHelpLink, - registryHostUrlWithPort: dataset.registryHostUrlWithPort, - repositoryUrl: dataset.repositoryUrl, - twoFactorAuthHelpLink: dataset.twoFactorAuthHelpLink, + registryData: { + endpoint: dataset.endpoint, + characterError: Boolean(dataset.characterError), + helpPagePath: dataset.helpPagePath, + noContainersImage: dataset.noContainersImage, + containersErrorImage: dataset.containersErrorImage, + repositoryUrl: dataset.repositoryUrl, + isGroupPage: dataset.isGroupPage, + personalAccessTokensHelpLink: dataset.personalAccessTokensHelpLink, + registryHostUrlWithPort: dataset.registryHostUrlWithPort, + twoFactorAuthHelpLink: dataset.twoFactorAuthHelpLink, + }, }; }, render(createElement) { return createElement('registry-app', { props: { - characterError: this.characterError, - containersErrorImage: this.containersErrorImage, - endpoint: this.endpoint, - helpPagePath: this.helpPagePath, - noContainersImage: this.noContainersImage, - personalAccessTokensHelpLink: this.personalAccessTokensHelpLink, - registryHostUrlWithPort: this.registryHostUrlWithPort, - repositoryUrl: this.repositoryUrl, - twoFactorAuthHelpLink: this.twoFactorAuthHelpLink, + ...this.registryData, }, }); }, diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index a2e0130e79e..2121f518a7a 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -20,7 +20,6 @@ export const fetchRepos = ({ commit, state }) => { export const fetchList = ({ commit }, { repo, page }) => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - return axios .get(repo.tagsPath, { params: { page } }) .then(response => { @@ -40,6 +39,7 @@ export const multiDeleteItems = (_, { path, items }) => axios.delete(path, { params: { ids: items } }); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); +export const setIsDeleteDisabled = ({ commit }, data) => commit(types.SET_IS_DELETE_DISABLED, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js index f4923512578..ac90bde1b2a 100644 --- a/app/assets/javascripts/registry/stores/getters.js +++ b/app/assets/javascripts/registry/stores/getters.js @@ -1,5 +1,6 @@ export const isLoading = state => state.isLoading; export const repos = state => state.repos; +export const isDeleteDisabled = state => state.isDeleteDisabled; // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js index 2c69bf11807..6740bfede1a 100644 --- a/app/assets/javascripts/registry/stores/mutation_types.js +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -1,4 +1,5 @@ export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT'; +export const SET_IS_DELETE_DISABLED = 'SET_IS_DELETE_DISABLED'; export const SET_REPOS_LIST = 'SET_REPOS_LIST'; export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING'; diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 8ace6657ad1..ea5925247d1 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -6,6 +6,10 @@ export default { Object.assign(state, { endpoint }); }, + [types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) { + Object.assign(state, { isDeleteDisabled }); + }, + [types.SET_REPOS_LIST](state, list) { Object.assign(state, { repos: list.map(el => ({ @@ -17,6 +21,7 @@ export default { location: el.location, name: el.path, tagsPath: el.tags_path, + projectId: el.project_id, })), }); }, diff --git a/app/assets/javascripts/registry/stores/state.js b/app/assets/javascripts/registry/stores/state.js index feeac10cbe1..724c64b4994 100644 --- a/app/assets/javascripts/registry/stores/state.js +++ b/app/assets/javascripts/registry/stores/state.js @@ -1,6 +1,7 @@ export default () => ({ isLoading: false, endpoint: '', // initial endpoint to fetch the repos list + isDeleteDisabled: false, // controls the delete buttons in the registry /** * Each object in `repos` has the following strucure: * { diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb index eab908ba5ed..15ef6698472 100644 --- a/app/controllers/boards/application_controller.rb +++ b/app/controllers/boards/application_controller.rb @@ -13,7 +13,7 @@ module Boards end def board_parent - @board_parent ||= board.parent + @board_parent ||= board.resource_parent end def record_not_found(exception) diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb index 90e04414d8d..880f7500708 100644 --- a/app/controllers/boards/lists_controller.rb +++ b/app/controllers/boards/lists_controller.rb @@ -9,7 +9,7 @@ module Boards skip_before_action :authenticate_user!, only: [:index] def index - lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board) + lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board) List.preload_preferences_for_user(lists, current_user) @@ -17,7 +17,7 @@ module Boards end def create - list = Boards::Lists::CreateService.new(board.parent, current_user, create_list_params).execute(board) + list = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board) if list.valid? render json: serialize_as_json(list) diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index 1ead631663e..672d31ec779 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -35,7 +35,7 @@ module MilestoneActions render json: tabs_json("shared/milestones/_labels_tab", { labels: milestone_labels.map do |label| - label.present(issuable_subject: @milestone.parent) + label.present(issuable_subject: @milestone.resource_parent) end }) end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 1eacae06457..1e9d51cf970 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -44,7 +44,7 @@ class Groups::MilestonesController < Groups::ApplicationController # all projects milestones states at once. milestones, update_params = get_milestones_for_update milestones.each do |milestone| - Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone) + Milestones::UpdateService.new(milestone.resource_parent, current_user, update_params).execute(milestone) end redirect_to milestone_path diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb new file mode 100644 index 00000000000..39f6963ee0a --- /dev/null +++ b/app/controllers/groups/registry/repositories_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true +module Groups + module Registry + class RepositoriesController < Groups::ApplicationController + before_action :verify_container_registry_enabled! + before_action :authorize_read_container_image! + + def index + track_event(:list_repositories) + + respond_to do |format| + format.html + format.json do + @images = group.container_repositories.with_api_entity_associations + + render json: ContainerRepositoriesSerializer + .new(current_user: current_user) + .represent(@images) + end + end + end + + private + + def verify_container_registry_enabled! + render_404 unless Gitlab.config.registry.enabled + end + + def authorize_read_container_image! + return render_404 unless can?(current_user, :read_container_image, group) + end + end + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 3c70ff3b59f..115d1031a8a 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -290,7 +290,8 @@ module ApplicationSettingsHelper :snowplow_cookie_domain, :snowplow_enabled, :snowplow_site_id, - :push_event_hooks_limit + :push_event_hooks_limit, + :push_event_activities_limit ] end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f05218efe0c..4f31cc67ccc 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -76,10 +76,10 @@ module GitlabRoutingHelper end def edit_milestone_path(entity, *args) - if entity.parent.is_a?(Group) - edit_group_milestone_path(entity.parent, entity, *args) + if entity.resource_parent.is_a?(Group) + edit_group_milestone_path(entity.resource_parent, entity, *args) else - edit_project_milestone_path(entity.parent, entity, *args) + edit_project_milestone_path(entity.resource_parent, entity, *args) end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 9cba87ac444..811467ca03a 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -15,6 +15,16 @@ module GroupsHelper %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] end + def group_packages_nav_link_paths + %w[ + groups/container_registries#index + ] + end + + def group_container_registry_nav? + Gitlab.config.registry.enabled && can?(current_user, :read_container_image, @group) + end + def group_sidebar_links @group_sidebar_links ||= get_group_sidebar_links end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index a919c068c42..dce0842060d 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -45,8 +45,8 @@ module TodosHelper end def todo_parent_path(todo) - if todo.parent.is_a?(Group) - link_to todo.parent.name, group_path(todo.parent) + if todo.resource_parent.is_a?(Group) + link_to todo.resource_parent.name, group_path(todo.resource_parent) else link_to_project(todo.project) end @@ -64,7 +64,7 @@ module TodosHelper if todo.for_commit? project_commit_path(todo.project, todo.target, path_options) else - path = [todo.parent, todo.target] + path = [todo.resource_parent, todo.target] path.unshift(:pipelines) if todo.build_failed? diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 0724ee8f39d..a07933d4975 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -217,6 +217,9 @@ class ApplicationSetting < ApplicationRecord validates :push_event_hooks_limit, numericality: { greater_than_or_equal_to: 0 } + validates :push_event_activities_limit, + numericality: { greater_than_or_equal_to: 0 } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index e9aab4a3d05..b341cf04403 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -83,6 +83,7 @@ module ApplicationSettingImplementation project_export_enabled: true, protected_ci_variables: false, push_event_hooks_limit: 3, + push_event_activities_limit: 3, raw_blob_request_limit: 300, recaptcha_enabled: false, login_recaptcha_protection_enabled: false, diff --git a/app/models/board.rb b/app/models/board.rb index 31011dc4742..f3f938224a4 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -16,10 +16,9 @@ class Board < ApplicationRecord !group end - def parent - @parent ||= group || project + def resource_parent + @resource_parent ||= group || project end - alias_method :resource_parent, :parent def group_board? group_id.present? diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index e055b66989b..27bb76835c7 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -11,6 +11,7 @@ class ContainerRepository < ApplicationRecord delegate :client, to: :registry scope :ordered, -> { order(:name) } + scope :with_api_entity_associations, -> { preload(:project) } # rubocop: disable CodeReuse/ServiceClass def registry diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 1d553fc8312..7d766e1f25c 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -11,7 +11,7 @@ class GlobalMilestone delegate :title, :state, :due_date, :start_date, :participants, :project, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, - :milestoneish_id, :parent, to: :milestone + :milestoneish_id, :resource_parent, to: :milestone def to_hash { diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 46cac1d41bb..0c36e51120f 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -23,6 +23,8 @@ class GpgSignature < ApplicationRecord validates :project_id, presence: true validates :gpg_key_primary_keyid, presence: true + scope :by_commit_sha, ->(shas) { where(commit_sha: shas) } + def self.with_key_and_subkeys(gpg_key) subkey_ids = gpg_key.subkeys.pluck(:id) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index ca50820a879..fe8ba9765b7 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -136,6 +136,7 @@ class MergeRequestDiff < ApplicationRecord # All diff information is collected from repository after object is created. # It allows you to override variables like head_commit_sha before getting diff. after_create :save_git_content, unless: :importing? + after_create_commit :set_as_latest_diff after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? } @@ -150,10 +151,6 @@ class MergeRequestDiff < ApplicationRecord # Collect information about commits and diff from repository # and save it to the database as serialized data def save_git_content - MergeRequest - .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id) - .update_all(latest_merge_request_diff_id: self.id) - ensure_commit_shas save_commits save_diffs @@ -168,6 +165,12 @@ class MergeRequestDiff < ApplicationRecord keep_around_commits end + def set_as_latest_diff + MergeRequest + .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id) + .update_all(latest_merge_request_diff_id: self.id) + end + def ensure_commit_shas self.start_commit_sha ||= merge_request.target_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha @@ -502,11 +505,6 @@ class MergeRequestDiff < ApplicationRecord merge_request.closed? && merge_request.metrics.latest_closed_at < EXTERNAL_DIFF_CUTOFF.ago end - # We can't rely on `merge_request.latest_merge_request_diff_id` because that - # may have been changed in `save_git_content` without being reflected in - # the association's instance. This query is always subject to races, but - # the worst case is that we *don't* make a diff external when we could. The - # background worker will make it external at a later date. def old_version? latest_id = MergeRequest .where(id: merge_request_id) @@ -514,7 +512,7 @@ class MergeRequestDiff < ApplicationRecord .pluck(:latest_merge_request_diff_id) .first - self.id != latest_id + latest_id && self.id < latest_id end def load_diffs(options) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 916c11a8d03..2fa0cfc9b93 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -257,10 +257,9 @@ class Milestone < ApplicationRecord title.to_slug.normalize.to_s end - def parent + def resource_parent group || project end - alias_method :resource_parent, :parent def group_milestone? group_id.present? diff --git a/app/models/note.rb b/app/models/note.rb index 3e645d79e15..43f349c6fa2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -483,10 +483,9 @@ class Note < ApplicationRecord Upload.find_by(model: self, path: paths) end - def parent + def resource_parent project end - alias_method :resource_parent, :parent private diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 4698df39730..6f7365a2763 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -26,6 +26,8 @@ class PushEvent < Event delegate :commit_count, to: :push_event_payload alias_method :commits_count, :commit_count + delegate :ref_count, to: :push_event_payload + # Returns events of pushes that either pushed to an existing ref or created a # new one. def self.created_or_pushed diff --git a/app/models/todo.rb b/app/models/todo.rb index 6b71845856a..456115872d1 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -144,10 +144,9 @@ class Todo < ApplicationRecord end end - def parent + def resource_parent project end - alias_method :resource_parent, :parent def unmergeable? action == UNMERGEABLE diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb index b8435dad3f1..e2b16249c85 100644 --- a/app/policies/board_policy.rb +++ b/app/policies/board_policy.rb @@ -3,7 +3,7 @@ class BoardPolicy < BasePolicy include FindGroupProjects - delegate { @subject.parent } + delegate { @subject.resource_parent } condition(:is_group_board) { @subject.group_board? } condition(:is_project_board) { @subject.project_board? } @@ -19,7 +19,7 @@ class BoardPolicy < BasePolicy condition(:reporter_of_group_projects) do next unless @user - group_projects_for(user: @user, group: @subject.parent) + group_projects_for(user: @user, group: @subject.resource_parent) .visible_to_user_and_access_level(@user, ::Gitlab::Access::REPORTER) .exists? end diff --git a/app/policies/milestone_policy.rb b/app/policies/milestone_policy.rb index 2d56eea6a78..9cea8ddd7b3 100644 --- a/app/policies/milestone_policy.rb +++ b/app/policies/milestone_policy.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class MilestonePolicy < BasePolicy - delegate { @subject.parent } + delegate { @subject.resource_parent } end diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb index cc746698a05..db9cf1c7835 100644 --- a/app/serializers/container_repository_entity.rb +++ b/app/serializers/container_repository_entity.rb @@ -18,7 +18,7 @@ class ContainerRepositoryEntity < Grape::Entity alias_method :repository, :object def project - request.project + request.respond_to?(:project) ? request.project : object.project end def can_destroy? diff --git a/app/services/bulk_push_event_payload_service.rb b/app/services/bulk_push_event_payload_service.rb new file mode 100644 index 00000000000..54157bc23f9 --- /dev/null +++ b/app/services/bulk_push_event_payload_service.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class BulkPushEventPayloadService + def initialize(event, push_data) + @event = event + @push_data = push_data + end + + def execute + @event.build_push_event_payload( + action: @push_data[:action], + commit_count: 0, + ref_count: @push_data[:ref_count], + ref_type: @push_data[:ref_type] + ) + + @event.push_event_payload.tap(&:save!) + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 395c5fe09ac..f7282c22a52 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -73,15 +73,27 @@ class EventCreateService end def push(project, current_user, push_data) + create_push_event(PushEventPayloadService, project, current_user, push_data) + end + + def bulk_push(project, current_user, push_data) + create_push_event(BulkPushEventPayloadService, project, current_user, push_data) + end + + private + + def create_record_event(record, current_user, status) + create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name) + end + + def create_push_event(service_class, project, current_user, push_data) # We're using an explicit transaction here so that any errors that may occur # when creating push payload data will result in the event creation being # rolled back as well. event = Event.transaction do new_event = create_event(project, current_user, Event::PUSHED) - PushEventPayloadService - .new(new_event, push_data) - .execute + service_class.new(new_event, push_data).execute new_event end @@ -92,12 +104,6 @@ class EventCreateService Users::ActivityService.new(current_user, 'push').execute end - private - - def create_record_event(record, current_user, status) - create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name) - end - def create_event(resource_parent, current_user, status, attributes = {}) attributes.reverse_merge!( action: status, diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index b1faef58e33..0801fd4d03f 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -48,6 +48,8 @@ module Git # Push events in the activity feed only show information for the # last commit. def create_events + return unless params.fetch(:create_push_event, true) + EventCreateService.new.push(project, current_user, event_push_data) end diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb index 62159d4e7e5..3052bed51bc 100644 --- a/app/services/git/process_ref_changes_service.rb +++ b/app/services/git/process_ref_changes_service.rb @@ -16,8 +16,8 @@ module Git def process_changes_by_action(ref_type, changes) changes_by_action = group_changes_by_action(changes) - changes_by_action.each do |_, changes| - process_changes(ref_type, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any? + changes_by_action.each do |action, changes| + process_changes(ref_type, action, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any? end end @@ -38,9 +38,11 @@ module Git (changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project) end - def process_changes(ref_type, changes, execute_project_hooks:) + def process_changes(ref_type, action, changes, execute_project_hooks:) push_service_class = push_service_class_for(ref_type) + create_bulk_push_event = changes.size > Gitlab::CurrentSettings.push_event_activities_limit + changes.each do |change| push_service_class.new( project, @@ -48,9 +50,20 @@ module Git change: change, push_options: params[:push_options], create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project), - execute_project_hooks: execute_project_hooks + execute_project_hooks: execute_project_hooks, + create_push_event: !create_bulk_push_event ).execute end + + create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event + end + + def create_bulk_push_event(ref_type, action, changes) + EventCreateService.new.bulk_push( + project, + current_user, + Gitlab::DataBuilder::Push.build_bulk(action: action, ref_type: ref_type, changes: changes) + ) end def push_service_class_for(ref_type) diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 076df10bf6f..7e6568b5b25 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -50,7 +50,7 @@ module Notes return if update_params.empty? return unless supported?(note) - self.class.noteable_update_service(note).new(note.parent, current_user, update_params).execute(note.noteable) + self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable) end end end diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb index 3c8847d3c18..e686d3bf7c2 100644 --- a/app/services/search/snippet_service.rb +++ b/app/services/search/snippet_service.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true module Search - class SnippetService - attr_accessor :current_user, :params - - def initialize(user, params) - @current_user, @params = user, params.dup - end - + class SnippetService < Search::GlobalService def execute Gitlab::SnippetSearchResults.new(current_user, params[:search]) end diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 22458223b93..6b02521a0f0 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -25,5 +25,10 @@ = f.number_field :push_event_hooks_limit, class: 'form-control' .form-text.text-muted = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.") + .form-group + = f.label :push_event_activities_limit, class: 'label-bold' + = f.number_field :push_event_activities_limit, class: 'form-control' + .form-text.text-muted + = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 21c418cb0e4..b9e88f3fc47 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -6,11 +6,13 @@ .event-title.d-flex.flex-wrap = inline_event_icon(event) - %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type} - %span.append-right-4 - - commits_link = project_commits_path(project, event.ref_name) - - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) - = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' + - many_refs = event.ref_count.to_i > 1 + %span.event-type.d-inline-block.append-right-4.pushed= many_refs ? "#{event.action_name} #{event.ref_count} #{event.ref_type.pluralize}" : "#{event.action_name} #{event.ref_type}" + - unless many_refs + %span.append-right-4 + - commits_link = project_commits_path(project, event.ref_name) + - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) + = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' = render "events/event_scope", event: event diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml new file mode 100644 index 00000000000..e85b0713230 --- /dev/null +++ b/app/views/groups/registry/repositories/index.html.haml @@ -0,0 +1,12 @@ +- page_title _("Container Registry") + +%section + .row.registry-placeholder.prepend-bottom-10 + .col-12 + #js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json), + "help_page_path" => help_page_path('user/packages/container_registry/index'), + "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), + "containers_error_image" => image_path('illustrations/docker-error-state.svg'), + "repository_url" => "", + is_group_page: true, + character_error: @character_error.to_s } } diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml new file mode 100644 index 00000000000..16b902a18b9 --- /dev/null +++ b/app/views/groups/sidebar/_packages.html.haml @@ -0,0 +1,16 @@ +- if group_container_registry_nav? + = nav_link(path: group_packages_nav_link_paths) do + = link_to group_container_registries_path(@group), title: _('Container Registry') do + .nav-icon-container + = sprite_icon('package') + %span.nav-item-name + = _('Packages') + %ul.sidebar-sub-level-items + = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do + = link_to group_container_registries_path(@group), title: _('Container Registry') do + %strong.fly-out-top-item-name + = _('Packages') + %li.divider.fly-out-top-item + = nav_link(controller: 'groups/container_registries') do + = link_to group_container_registries_path(@group), title: _('Container Registry') do + %span= _('Container Registry') diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 7cc7d1783c4..4930c6cf5f7 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -118,7 +118,7 @@ %strong.fly-out-top-item-name = _('Kubernetes') - = render_if_exists 'groups/sidebar/packages' # EE-specific + = render_if_exists 'groups/sidebar/packages' - if group_sidebar_link?(:group_members) = nav_link(path: 'group_members#index') do diff --git a/app/views/shared/boards/_switcher.html.haml b/app/views/shared/boards/_switcher.html.haml index 79118630762..09a365a290a 100644 --- a/app/views/shared/boards/_switcher.html.haml +++ b/app/views/shared/boards/_switcher.html.haml @@ -1,4 +1,4 @@ -- parent = board.parent +- parent = board.resource_parent - milestone_filter_opts = { format: :json } - milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board? - weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : [] diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml index 416b4a34651..ae0e5e45afe 100644 --- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -3,6 +3,6 @@ Add list .dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, board.parent) + - if can?(current_user, :admin_label, board.resource_parent) = render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' } = dropdown_loading diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index c9458475aa5..9165147ef2a 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -2,7 +2,7 @@ - board = local_assigns.fetch(:board, nil) - is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics - block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : '' -- user_can_admin_list = board && can?(current_user, :admin_list, board.parent) +- user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent) .issues-filters{ class: ("w-100" if type == :boards_modal) } .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal } diff --git a/changelogs/unreleased/17970-preserve-leading-whitespace.yml b/changelogs/unreleased/17970-preserve-leading-whitespace.yml new file mode 100644 index 00000000000..84085b80547 --- /dev/null +++ b/changelogs/unreleased/17970-preserve-leading-whitespace.yml @@ -0,0 +1,5 @@ +--- +title: Prevent the slash command parser from removing leading whitespace from content that is unrelated to slash commands +merge_request: 18589 +author: Jared Deckard +type: fixed diff --git a/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml new file mode 100644 index 00000000000..463d8a0ab98 --- /dev/null +++ b/changelogs/unreleased/21800-parse-mentioned-users-group-projects-from-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Adds separate parsers for mentions of users, groups, projects in markdown content +merge_request: 18318 +author: +type: added diff --git a/changelogs/unreleased/23315-group-level-container-registry-browser.yml b/changelogs/unreleased/23315-group-level-container-registry-browser.yml new file mode 100644 index 00000000000..4340c565a88 --- /dev/null +++ b/changelogs/unreleased/23315-group-level-container-registry-browser.yml @@ -0,0 +1,5 @@ +--- +title: Group level Container Registry browser +merge_request: 17615 +author: +type: added diff --git a/changelogs/unreleased/2358-elasticsearch-project-snippets.yml b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml new file mode 100644 index 00000000000..28324c1827d --- /dev/null +++ b/changelogs/unreleased/2358-elasticsearch-project-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Support ES searches for project snippets +merge_request: 18459 +author: +type: fixed diff --git a/changelogs/unreleased/31007-limit-activity-events.yml b/changelogs/unreleased/31007-limit-activity-events.yml new file mode 100644 index 00000000000..d5ad588af33 --- /dev/null +++ b/changelogs/unreleased/31007-limit-activity-events.yml @@ -0,0 +1,5 @@ +--- +title: Aggregate push events when there are too many +merge_request: 18239 +author: +type: changed diff --git a/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml new file mode 100644 index 00000000000..e909c56983b --- /dev/null +++ b/changelogs/unreleased/31441-make-it-easy-for-includes-to-add-jobs-at-beginning-end-of-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Add two new predefined stages to pipelines +merge_request: 18205 +author: +type: added diff --git a/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml new file mode 100644 index 00000000000..979250d4762 --- /dev/null +++ b/changelogs/unreleased/id-cleanup-anny-approver-migrations.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup background migrations for any approval rules +merge_request: 18256 +author: +type: changed diff --git a/changelogs/unreleased/id-fix-nplus1-for-signatures.yml b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml new file mode 100644 index 00000000000..e060c771227 --- /dev/null +++ b/changelogs/unreleased/id-fix-nplus1-for-signatures.yml @@ -0,0 +1,5 @@ +--- +title: Remove N+1 for fetching commits signatures +merge_request: 18389 +author: +type: performance diff --git a/changelogs/unreleased/sh-move-mr-diff-after-commit.yml b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml new file mode 100644 index 00000000000..7eb1edcfe4f --- /dev/null +++ b/changelogs/unreleased/sh-move-mr-diff-after-commit.yml @@ -0,0 +1,5 @@ +--- +title: Reduce idle in transaction time when updating a merge request +merge_request: 18493 +author: +type: performance diff --git a/changelogs/unreleased/update-gitlab-shell-10-2.yml b/changelogs/unreleased/update-gitlab-shell-10-2.yml new file mode 100644 index 00000000000..cc13c18d633 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-shell-10-2.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Shell to v10.2.0 +merge_request: 18735 +author: +type: other diff --git a/config/routes/group.rb b/config/routes/group.rb index 1baac9874a2..093cde64c85 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -77,6 +77,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do post :pause end end + + resources :container_registries, only: [:index], controller: 'registry/repositories' end scope(path: '*id', diff --git a/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb new file mode 100644 index 00000000000..84befc95d00 --- /dev/null +++ b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddPushEventActivitiesLimitToApplicationSettings < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :push_event_activities_limit, :integer, default: 3) + end + + def down + remove_column(:application_settings, :push_event_activities_limit) + end +end diff --git a/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb new file mode 100644 index 00000000000..72621971dbb --- /dev/null +++ b/db/migrate/20191008142331_add_ref_count_to_push_event_payloads.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddRefCountToPushEventPayloads < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :push_event_payloads, :ref_count, :integer + end +end diff --git a/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb new file mode 100644 index 00000000000..e1c0f1d6c0c --- /dev/null +++ b/db/post_migrate/20191007163701_populate_remaining_any_approver_rules_for_merge_requests.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateRemainingAnyApproverRulesForMergeRequests < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 10_000 + MIGRATION = 'PopulateAnyApprovalRuleForMergeRequests' + + disable_ddl_transaction! + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) } + end + + def up + return unless Gitlab.ee? + + add_concurrent_index :merge_requests, :id, + name: 'tmp_merge_requests_with_approvals_before_merge', + where: 'approvals_before_merge != 0' + + Gitlab::BackgroundMigration.steal(MIGRATION) + + PopulateRemainingAnyApproverRulesForMergeRequests::MergeRequest.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForMergeRequests.new.perform(*range) + end + + remove_concurrent_index_by_name(:merge_requests, 'tmp_merge_requests_with_approvals_before_merge') + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb new file mode 100644 index 00000000000..fce17ffcf16 --- /dev/null +++ b/db/post_migrate/20191007163736_populate_remaining_any_approver_rules_for_projects.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateRemainingAnyApproverRulesForProjects < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 5_000 + MIGRATION = 'PopulateAnyApprovalRuleForProjects' + + disable_ddl_transaction! + + class Project < ActiveRecord::Base + include EachBatch + + self.table_name = 'projects' + + scope :with_approvals_before_merge, -> { where.not(approvals_before_merge: 0) } + end + + def up + return unless Gitlab.ee? + + add_concurrent_index :projects, :id, + name: 'tmp_projects_with_approvals_before_merge', + where: 'approvals_before_merge != 0' + + Gitlab::BackgroundMigration.steal(MIGRATION) + + PopulateRemainingAnyApproverRulesForProjects::Project.with_approvals_before_merge.each_batch(of: BATCH_SIZE) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::PopulateAnyApprovalRuleForProjects.new.perform(*range) + end + + remove_concurrent_index_by_name(:projects, 'tmp_projects_with_approvals_before_merge') + end + + def down + # no-op + end +end diff --git a/db/schema.rb b/db/schema.rb index 50627fc9b06..825b66f6dfd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -339,6 +339,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600 t.integer "throttle_incident_management_notification_per_period", default: 3600 t.integer "push_event_hooks_limit", default: 3, null: false + t.integer "push_event_activities_limit", default: 3, null: false t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" @@ -3158,6 +3159,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do t.binary "commit_to" t.text "ref" t.string "commit_title", limit: 70 + t.integer "ref_count" t.index ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true end diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 5dcdf0e85e9..4152f31c726 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -86,7 +86,8 @@ Below we describe how to configure two Gitaly servers one at `gitaly1.internal` and the other at `gitaly2.internal` with secret token `abc123secret`. We assume your GitLab installation has three repository storages: `default`, -`storage1` and `storage2`. +`storage1` and `storage2`. You can use as little as just one server with one +repository storage if desired. ### 1. Installation @@ -129,7 +130,7 @@ Configure a token on the instance that runs the GitLab Rails application. Next, on the Gitaly servers, you need to configure storage paths, enable the network listener and configure the token. -NOTE: **Note:** if you want to reduce the risk of downtime when you enable +NOTE: **Note:** If you want to reduce the risk of downtime when you enable authentication you can temporarily disable enforcement, see [the documentation on configuring Gitaly authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication) @@ -177,20 +178,19 @@ Check the directory layout on your Gitaly server to be sure. # Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server. gitlab_rails['internal_api_url'] = 'https://gitlab.example.com' + # Authentication token to ensure only authorized servers can communicate with + # Gitaly server + gitaly['auth_token'] = 'abc123secret' + # Make Gitaly accept connections on all network interfaces. You must use # firewalls to restrict access to this address/port. + # Comment out following line if you only want to support TLS connections gitaly['listen_addr'] = "0.0.0.0:8075" - gitaly['auth_token'] = 'abc123secret' - - # To use TLS for Gitaly you need to add - gitaly['tls_listen_addr'] = "0.0.0.0:9999" - gitaly['certificate_path'] = "path/to/cert.pem" - gitaly['key_path'] = "path/to/key.pem" ``` 1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: - For `gitaly1.internal`: + On `gitaly1.internal`: ``` gitaly['storage'] = [ @@ -199,7 +199,7 @@ Check the directory layout on your Gitaly server to be sure. ] ``` - For `gitaly2.internal`: + On `gitaly2.internal`: ``` gitaly['storage'] = [ @@ -219,11 +219,6 @@ Check the directory layout on your Gitaly server to be sure. ```toml listen_addr = '0.0.0.0:8075' - tls_listen_addr = '0.0.0.0:9999' - - [tls] - certificate_path = /path/to/cert.pem - key_path = /path/to/key.pem [auth] token = 'abc123secret' @@ -231,7 +226,7 @@ Check the directory layout on your Gitaly server to be sure. 1. Append the following to `/home/git/gitaly/config.toml` for each respective server: - For `gitaly1.internal`: + On `gitaly1.internal`: ```toml [[storage]] @@ -241,7 +236,7 @@ Check the directory layout on your Gitaly server to be sure. name = 'storage1' ``` - For `gitaly2.internal`: + On `gitaly2.internal`: ```toml [[storage]] @@ -369,11 +364,12 @@ To disable Gitaly on a client node: > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22602) in GitLab 11.8. Gitaly supports TLS encryption. To be able to communicate -with a Gitaly instance that listens for secure connections you will need to use `tls://` url +with a Gitaly instance that listens for secure connections you will need to use `tls://` URL scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration. You will need to bring your own certificates as this isn't provided automatically. -The certificate to be used needs to be installed on all Gitaly nodes and on all +The certificate to be used needs to be installed on all Gitaly nodes, and the +certificate (or CA of certificate) on all client nodes that communicate with it following the procedure described in [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). @@ -395,7 +391,7 @@ To configure Gitaly with TLS: **For Omnibus GitLab** -1. On the client nodes, edit `/etc/gitlab/gitlab.rb`: +1. On the client node(s), edit `/etc/gitlab/gitlab.rb` as follows: ```ruby git_data_dirs({ @@ -407,20 +403,38 @@ To configure Gitaly with TLS: gitlab_rails['gitaly_token'] = 'abc123secret' ``` -1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). -1. On the Gitaly server nodes, edit `/etc/gitlab/gitlab.rb`: +1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on client node(s). +1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: + + ```sh + sudo mkdir -p /etc/gitlab/ssl + sudo chmod 700 /etc/gitlab/ssl + sudo cp key.pem cert.pem /etc/gitlab/ssl/ + ``` + +1. On the Gitaly server node(s), edit `/etc/gitlab/gitlab.rb` and add: + + <!-- + updates to following example must also be made at + https://gitlab.com/gitlab-org/charts/gitlab/blob/master/doc/advanced/external-gitaly/external-omnibus-gitaly.md#configure-omnibus-gitlab + --> ```ruby gitaly['tls_listen_addr'] = "0.0.0.0:9999" - gitaly['certificate_path'] = "path/to/cert.pem" - gitaly['key_path'] = "path/to/key.pem" + gitaly['certificate_path'] = "/etc/gitlab/ssl/cert.pem" + gitaly['key_path'] = "/etc/gitlab/ssl/key.pem" ``` -1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). +1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on Gitaly server node(s). +1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections), + you can improve security by disabling non-TLS connections by commenting out + or deleting `gitaly['listen_addr']` in `/etc/gitlab/gitlab.rb`, saving the file, + and [reconfiguring GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) + on Gitaly server node(s). **For installations from source** -1. On the client nodes, edit `/home/git/gitlab/config/gitlab.yml`: +1. On the client node(s), edit `/home/git/gitlab/config/gitlab.yml` as follows: ```yaml gitlab: @@ -445,18 +459,33 @@ To configure Gitaly with TLS: data will be stored in this folder. This will no longer be necessary after [this issue](https://gitlab.com/gitlab-org/gitaly/issues/1282) is resolved. -1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source). -1. On the Gitaly server nodes, edit `/home/git/gitaly/config.toml`: +1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on client node(s). +1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there: + + ```sh + sudo mkdir -p /etc/gitlab/ssl + sudo chmod 700 /etc/gitlab/ssl + sudo cp key.pem cert.pem /etc/gitlab/ssl/ + ``` + +1. On the Gitaly server node(s), edit `/home/git/gitaly/config.toml` and add: ```toml tls_listen_addr = '0.0.0.0:9999' [tls] - certificate_path = '/path/to/cert.pem' - key_path = '/path/to/key.pem' + certificate_path = '/etc/gitlab/ssl/cert.pem' + key_path = '/etc/gitlab/ssl/key.pem' ``` -1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source). +1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on Gitaly server node(s). +1. (Optional) After [verifying that all Gitaly traffic is being served over TLS](#observe-type-of-gitaly-connections), + you can improve security by disabling non-TLS connections by commenting out + or deleting `listen_addr` in `/home/git/gitaly/config.toml`, saving the file, + and [restarting GitLab](../restart_gitlab.md#installations-from-source) + on Gitaly server node(s). + +### Observe type of Gitaly connections To observe what type of connections are actually being used in a production environment you can use the following Prometheus query: diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 9e47f7767fe..9038675a28f 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -68,20 +68,26 @@ sidekiq['enable'] = false gitlab_workhorse['enable'] = false gitaly['enable'] = false +# virtual_storage_name must match the same storage name given to praefect in git_data_dirs +praefect['virtual_storage_name'] = 'praefect' +praefect['auth_token'] = 'super_secret_abc' praefect['enable'] = true praefect['storage_nodes'] = [ { 'storage' => 'praefect-git-1', 'address' => 'tcp://praefect-git-1.internal', + 'token' => 'token1', 'primary' => true }, { 'storage' => 'praefect-git-2', - 'address' => 'tcp://praefect-git-2.internal' + 'address' => 'tcp://praefect-git-2.internal', + 'token' => 'token2' }, { 'storage' => 'praefect-git-3', - 'address' => 'tcp://praefect-git-3.internal' + 'address' => 'tcp://praefect-git-3.internal', + 'token' => 'token3' } ] ``` diff --git a/doc/api/settings.md b/doc/api/settings.md index 24e6f90e844..2d9e435bbb6 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -290,6 +290,7 @@ are listed in the descriptions of the relevant settings. | `protected_ci_variables` | boolean | no | Environment variables are protected by default. | | `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory. | `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value. | +| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events will be created. [Bulk push events will be created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. | | `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. | | `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. | | `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. | diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index e9157951c7b..c60b3323105 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -29,7 +29,12 @@ If you want to disable it for a specific project, you can do so in ## Maximum artifacts size **(CORE ONLY)** The maximum size of the [job artifacts](../../../administration/job_artifacts.md) -can be set at the project level, group level, and at the instance level. The value is: +can be set at: + +- The instance level. +- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/21688), the project and group level. + +The value is: - In *MB* and the default is 100MB per job. - [Set to 1G](../../gitlab_com/index.md#gitlab-cicd) on GitLab.com. diff --git a/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png Binary files differnew file mode 100644 index 00000000000..38e666e32ac --- /dev/null +++ b/doc/user/admin_area/settings/img/bulk_push_event_v12_4.png diff --git a/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png Binary files differnew file mode 100644 index 00000000000..fd3775ac4d7 --- /dev/null +++ b/doc/user/admin_area/settings/img/push_event_activities_limit_v12_4.png diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index ff86620dbb2..4ca91ae5339 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -22,6 +22,7 @@ include: - [Custom templates repository](instance_template_repository.md) **(PREMIUM)** - [Protected paths](protected_paths.md) **(CORE ONLY)** - [Help messages for the `/help` page and the login page](help_page.md) +- [Push event activities limit and bulk push events](push_event_activities_limit.md) NOTE: **Note:** You can change the [first day of the week](../../profile/preferences.md) for the entire GitLab instance diff --git a/doc/user/admin_area/settings/push_event_activities_limit.md b/doc/user/admin_area/settings/push_event_activities_limit.md new file mode 100644 index 00000000000..9850de0f4b3 --- /dev/null +++ b/doc/user/admin_area/settings/push_event_activities_limit.md @@ -0,0 +1,28 @@ +--- +type: reference +--- + +# Push event activities limit and bulk push events + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31007) in GitLab 12.4. + +This allows you to set the number of changes (branches or tags) in a single push +to determine whether individual push events or bulk push event will be created. +Bulk push events will be created if it surpasses that value. + +For example, if 4 branches are pushed and the limit is currently set to 3, +you'll see the following in the activity feed: + +![Bulk push event](img/bulk_push_event_v12_4.png) + +With this feature, when a single push includes a lot of changes (e.g. 1,000 +branches), only 1 bulk push event will be created instead of creating 1,000 push +events. This helps in maintaining good system performance and preventing spam on +the activity feed. + +This setting can be modified in **Admin Area > Settings > Network > Performance Optimization**. +This can also be configured via the [Application settings API](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls) +as `push_event_activities_limit`. The default value is 3, but it can be greater +than or equal 0. + +![Push event activities limit](img/push_event_activities_limit_v12_4.png) diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 49a8643d82d..c4be08c842b 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -178,9 +178,9 @@ There are two different ways to add a new project to a group: ### Default project-creation level -> [Introduced][ee-2534] in [GitLab Premium][ee] 10.5. -> Brought to [GitLab Starter][ee] in 10.7. -> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10. +> - [Introduced][ee-2534] in [GitLab Premium][ee] 10.5. +> - Brought to [GitLab Starter][ee] in 10.7. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10. By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under a group. @@ -338,8 +338,7 @@ request to add a new user to a project through API will not be possible. #### IP access restriction **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in -[GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1985) in [GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0. To make sure only people from within your organization can access particular resources, you have the option to restrict access to groups and their @@ -351,16 +350,20 @@ Add one or more whitelisted IP subnets using CIDR notation in comma separated fo coming from a different IP address won't be able to access the restricted content. -Restriction currently applies to UI, API access and Git actions via SSH. +Restriction currently applies to: + +- UI. +- API access. +- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/32113), Git actions via SSH. + To avoid accidental lock-out, admins and group owners are are able to access the group regardless of the IP restriction. #### Allowed domain restriction **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in -[GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2. -You can restrict access to groups and their underlying projects by +You can restrict access to groups by allowing only users with email addresses in particular domains to be added to the group. Add email domains you want to whitelist and users with emails from different diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 522f3ed5565..897adef5f58 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -933,8 +933,8 @@ module API end class PushEventPayload < Grape::Entity - expose :commit_count, :action, :ref_type, :commit_from, :commit_to - expose :ref, :commit_title + expose :commit_count, :action, :ref_type, :commit_from, :commit_to, :ref, + :commit_title, :ref_count end class Event < Grape::Entity @@ -988,11 +988,11 @@ module API def todo_target_url(todo) target_type = todo.target_type.underscore - target_url = "#{todo.parent.class.to_s.underscore}_#{target_type}_url" + target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url" Gitlab::Routing .url_helpers - .public_send(target_url, todo.parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend + .public_send(target_url, todo.resource_parent, todo.target, anchor: todo_target_anchor(todo)) # rubocop:disable GitlabSecurity/PublicSend end def todo_target_anchor(todo) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b7a471f14fe..c90ba0c9b5d 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -102,6 +102,7 @@ module API optional :project_export_enabled, type: Boolean, desc: 'Enable project export' optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics' optional :push_event_hooks_limit, type: Integer, desc: "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value." + optional :push_event_activities_limit, type: Integer, desc: 'Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.' optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts' given recaptcha_enabled: ->(val) { val } do requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' diff --git a/lib/banzai/reference_parser/mentioned_user_parser.rb b/lib/banzai/reference_parser/mentioned_user_parser.rb new file mode 100644 index 00000000000..4b1bcb3ca09 --- /dev/null +++ b/lib/banzai/reference_parser/mentioned_user_parser.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class MentionedUserParser < BaseParser + self.reference_type = :user + + def references_relation + User + end + + # any user can be mentioned by username + def can_read_reference?(user, ref_attr, node) + true + end + end + end +end diff --git a/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb new file mode 100644 index 00000000000..d4ff6a12cd0 --- /dev/null +++ b/lib/banzai/reference_parser/mentioned_users_by_group_parser.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class MentionedUsersByGroupParser < BaseParser + GROUP_ATTR = 'data-group' + + self.reference_type = :user + + def self.data_attribute + @data_attribute ||= GROUP_ATTR + end + + def references_relation + Group + end + + def nodes_visible_to_user(user, nodes) + groups = lazy { grouped_objects_for_nodes(nodes, Group, GROUP_ATTR) } + + nodes.select do |node| + node.has_attribute?(GROUP_ATTR) && can_read_group_reference?(node, user, groups) + end + end + + def can_read_group_reference?(node, user, groups) + node_group = groups[node] + + node_group && can?(user, :read_group, node_group) + end + end + end +end diff --git a/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb new file mode 100644 index 00000000000..79258d81cc3 --- /dev/null +++ b/lib/banzai/reference_parser/mentioned_users_by_project_parser.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class MentionedUsersByProjectParser < ProjectParser + PROJECT_ATTR = 'data-project' + + self.reference_type = :user + + def self.data_attribute + @data_attribute ||= PROJECT_ATTR + end + + def references_relation + Project + end + end + end +end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 342dcb2f784..9c1e6277e95 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -78,8 +78,13 @@ module Gitlab def build_config(config) initial_config = Gitlab::Config::Loader::Yaml.new(config).load! initial_config = Config::External::Processor.new(initial_config, @context).perform + initial_config = Config::Extendable.new(initial_config).to_hash - Config::Extendable.new(initial_config).to_hash + if Feature.enabled?(:ci_pre_post_pipeline_stages, @context.project, default_enabled: true) + initial_config = Config::EdgeStagesInjector.new(initial_config).to_hash + end + + initial_config end def build_context(project:, sha:, user:) diff --git a/lib/gitlab/ci/config/edge_stages_injector.rb b/lib/gitlab/ci/config/edge_stages_injector.rb new file mode 100644 index 00000000000..64ff9f951e4 --- /dev/null +++ b/lib/gitlab/ci/config/edge_stages_injector.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + class EdgeStagesInjector + PRE_PIPELINE = '.pre' + POST_PIPELINE = '.post' + EDGES = [PRE_PIPELINE, POST_PIPELINE].freeze + + def self.wrap_stages(stages) + stages = stages.to_a - EDGES + stages.unshift PRE_PIPELINE + stages.push POST_PIPELINE + + stages + end + + def initialize(config) + @config = config.to_h.deep_dup + end + + def to_hash + if config.key?(:stages) + process(:stages) + elsif config.key?(:types) + process(:types) + else + config + end + end + + private + + attr_reader :config + + delegate :wrap_stages, to: :class + + def process(keyword) + stages = extract_stages(keyword) + return config if stages.empty? + + stages = wrap_stages(stages) + config[keyword] = stages + config + end + + def extract_stages(keyword) + stages = config[keyword] + return [] unless stages.is_a?(Array) + + stages + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb index 2d715cbc6bb..7e431f0f8bb 100644 --- a/lib/gitlab/ci/config/entry/stages.rb +++ b/lib/gitlab/ci/config/entry/stages.rb @@ -15,7 +15,7 @@ module Gitlab end def self.default - %w[build test deploy] + Config::EdgeStagesInjector.wrap_stages %w[build test deploy] end end end diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 3460e07fdc5..a83b03f540c 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -107,6 +107,14 @@ module Gitlab } end + def build_bulk(action:, ref_type:, changes:) + { + action: action, + ref_count: changes.count, + ref_type: ref_type + } + end + # This method provides a sample data generated with # existing project and commits to test webhooks def build_sample(project, user) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 4b797a0e397..dc71d0b427a 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -10,6 +10,8 @@ module Gitlab repo = commit.project.repository.raw_repository @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) + + lazy_signature end def signature_text @@ -28,18 +30,16 @@ module Gitlab !!(signature_text && signed_text) end - # rubocop: disable CodeReuse/ActiveRecord def signature return unless has_signature? return @signature if @signature - cached_signature = GpgSignature.find_by(commit_sha: @commit.sha) + cached_signature = lazy_signature&.itself return @signature = cached_signature if cached_signature.present? @signature = create_cached_signature! end - # rubocop: enable CodeReuse/ActiveRecord def update_signature!(cached_signature) using_keychain do |gpg_key| @@ -50,6 +50,14 @@ module Gitlab private + def lazy_signature + BatchLoader.for(@commit.sha).batch do |shas, loader| + GpgSignature.by_commit_sha(shas).each do |signature| + loader.call(signature.commit_sha, signature) + end + end + end + def using_keychain Gitlab::Gpg.using_tmp_keychain do # first we need to get the fingerprint from the signature to query the gpg diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb index 4de95edfc18..5ba7b29734b 100644 --- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb +++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb @@ -14,6 +14,29 @@ module Gitlab def log_filename File.join(Rails.root, 'log', 'sidekiq_exporter.log') end + + private + + # Sidekiq Exporter does not work properly in sidekiq-cluster + # mode. It tries to start the service on the same port for + # each of the cluster workers, this results in failure + # due to duplicate binding. + # + # For now we ignore this error, as metrics are still "kind of" + # valid as they are rendered from shared directory. + # + # Issue: https://gitlab.com/gitlab-org/gitlab/issues/5714 + def start_working + super + rescue Errno::EADDRINUSE => e + Sidekiq.logger.error( + class: self.class.to_s, + message: 'Cannot start sidekiq_exporter', + exception: e.message + ) + + false + end end end end diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index ff9bb293b47..e04d6f250b1 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -50,7 +50,7 @@ module Gitlab content, commands = perform_substitutions(content, commands) - [content.strip, commands] + [content.rstrip, commands] end private @@ -109,7 +109,7 @@ module Gitlab [ ] (?<arg>[^\n]*) )? - (?:\n|$) + (?:\s*\n|$) ) }mix end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 00f817c2399..ea2b03b42c1 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -3,7 +3,8 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor - REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze + REFERABLES = %i(user issue label milestone + merge_request snippet commit commit_range directly_addressed_user epic).freeze attr_accessor :project, :current_user, :author def initialize(project, current_user = nil) @@ -54,9 +55,9 @@ module Gitlab def self.references_pattern return @pattern if @pattern - patterns = REFERABLES.map do |ref| - ref.to_s.classify.constantize.try(:reference_pattern) - end + patterns = REFERABLES.map do |type| + Banzai::ReferenceParser[type].reference_type.to_s.classify.constantize.try(:reference_pattern) + end.uniq @pattern = Regexp.union(patterns.compact) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dae5327c398..e17d7c29c4b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4352,7 +4352,7 @@ msgstr "" msgid "ContainerRegistry|Docker connection error" msgstr "" -msgid "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." +msgid "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." msgstr "" msgid "ContainerRegistry|Last Updated" @@ -4384,6 +4384,9 @@ msgstr "" msgid "ContainerRegistry|Tag ID" msgstr "" +msgid "ContainerRegistry|There are no container images available in this group" +msgstr "" + msgid "ContainerRegistry|There are no container images stored for this project" msgstr "" @@ -4393,6 +4396,9 @@ msgstr "" msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgstr "" +msgid "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}" +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}" msgstr "" @@ -11193,6 +11199,9 @@ msgstr "" msgid "Number of LOCs per commit" msgstr "" +msgid "Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value." +msgstr "" + msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value." msgstr "" diff --git a/package.json b/package.json index 29fb22d4caa..54f803a8964 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.6.2", "@gitlab/svgs": "^1.78.0", - "@gitlab/ui": "5.32.0", + "@gitlab/ui": "5.35.0", "@gitlab/visual-review-tools": "1.0.3", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml index 3ccf2d62ae4..573a5ccde11 100644 --- a/scripts/review_apps/base-config.yaml +++ b/scripts/review_apps/base-config.yaml @@ -62,10 +62,10 @@ gitlab: unicorn: resources: requests: - cpu: 600m + cpu: 400m memory: 1.4G limits: - cpu: 1.2G + cpu: 800m memory: 2.8G deployment: readinessProbe: @@ -95,10 +95,10 @@ gitlab-runner: minio: resources: requests: - cpu: 100m + cpu: 5m memory: 128M limits: - cpu: 200m + cpu: 10m memory: 280M nginx-ingress: controller: @@ -107,10 +107,10 @@ nginx-ingress: replicaCount: 2 resources: requests: - cpu: 150m + cpu: 100m memory: 250M limits: - cpu: 300m + cpu: 200m memory: 500M minAvailable: 1 service: diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb new file mode 100644 index 00000000000..4129891914d --- /dev/null +++ b/spec/controllers/groups/registry/repositories_controller_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Groups::Registry::RepositoriesController do + let_it_be(:user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:group, reload: true) { create(:group) } + + before do + stub_container_registry_config(enabled: true) + group.add_owner(user) + group.add_guest(guest) + sign_in(user) + end + + context 'GET #index' do + context 'when container registry is enabled' do + it 'show index page' do + get :index, params: { + group_id: group + } + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'has the correct response schema' do + get :index, params: { + group_id: group, + format: :json + } + + expect(response).to match_response_schema('registry/repositories') + end + + it 'returns a list of projects for json format' do + project = create(:project, group: group) + repo = create(:container_repository, project: project) + + get :index, params: { + group_id: group, + format: :json + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Array) + expect(json_response.first).to include( + 'id' => repo.id, + 'name' => repo.name + ) + end + + it 'tracks the event' do + expect(Gitlab::Tracking).to receive(:event).with(anything, 'list_repositories', {}) + + get :index, params: { + group_id: group + } + end + end + + context 'container registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'renders not found' do + get :index, params: { + group_id: group + } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'user do not have acces to container registry' do + before do + sign_out(user) + sign_in(guest) + end + + it 'renders not found' do + get :index, params: { + group_id: group + } + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index 29cfe8fb295..a201ca94380 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -7,7 +7,7 @@ FactoryBot.define do group { nil } project_id { nil } group_id { nil } - parent { nil } + resource_parent { nil } end after(:build, :stub) do |board, evaluator| @@ -19,9 +19,9 @@ FactoryBot.define do board.project = evaluator.project elsif evaluator.project_id board.project_id = evaluator.project_id - elsif evaluator.parent - id = evaluator.parent.id - evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id + elsif evaluator.resource_parent + id = evaluator.resource_parent.id + evaluator.resource_parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id else board.project = create(:project, :empty_repo) end diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb index 75ff925774a..32eee645f6a 100644 --- a/spec/factories/milestones.rb +++ b/spec/factories/milestones.rb @@ -9,7 +9,7 @@ FactoryBot.define do group { nil } project_id { nil } group_id { nil } - parent { nil } + resource_parent { nil } end trait :active do @@ -34,9 +34,9 @@ FactoryBot.define do milestone.project = evaluator.project elsif evaluator.project_id milestone.project_id = evaluator.project_id - elsif evaluator.parent - id = evaluator.parent.id - evaluator.parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id + elsif evaluator.resource_parent + id = evaluator.resource_parent.id + evaluator.resource_parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id else milestone.project = create(:project) end diff --git a/spec/features/groups/user_sees_package_sidebar_spec.rb b/spec/features/groups/user_sees_package_sidebar_spec.rb new file mode 100644 index 00000000000..f85b6841636 --- /dev/null +++ b/spec/features/groups/user_sees_package_sidebar_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Groups > sidebar' do + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + group.add_developer(user) + sign_in(user) + end + + context 'Package menu' do + context 'when container registry is enabled' do + before do + stub_container_registry_config(enabled: true) + visit group_path(group) + end + + it 'shows main menu' do + within '.nav-sidebar' do + expect(page).to have_link(_('Packages')) + end + end + + it 'has container registry link' do + within '.nav-sidebar' do + expect(page).to have_link(_('Container Registry')) + end + end + end + + context 'when container registry is disabled' do + before do + stub_container_registry_config(enabled: false) + visit group_path(group) + end + + it 'does not have container registry link' do + within '.nav-sidebar' do + expect(page).not_to have_link(_('Container Registry')) + end + end + end + end +end diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb index 7c1d88f7798..fd72f2dfefa 100644 --- a/spec/features/milestones/user_deletes_milestone_spec.rb +++ b/spec/features/milestones/user_deletes_milestone_spec.rb @@ -12,7 +12,7 @@ describe "User deletes milestone", :js do end context "when milestone belongs to project" do - let!(:milestone) { create(:milestone, parent: project, title: "project milestone") } + let!(:milestone) { create(:milestone, resource_parent: project, title: "project milestone") } it "deletes milestone" do project.add_developer(user) @@ -30,8 +30,8 @@ describe "User deletes milestone", :js do end context "when milestone belongs to group" do - let!(:milestone_to_be_deleted) { create(:milestone, parent: group, title: "group milestone 1") } - let!(:milestone) { create(:milestone, parent: group, title: "group milestone 2") } + let!(:milestone_to_be_deleted) { create(:milestone, resource_parent: group, title: "group milestone 1") } + let!(:milestone) { create(:milestone, resource_parent: group, title: "group milestone 2") } it "deletes milestone" do group.add_developer(user) diff --git a/spec/finders/boards/visits_finder_spec.rb b/spec/finders/boards/visits_finder_spec.rb index 4d40f4826f8..7e3ad8aa9f0 100644 --- a/spec/finders/boards/visits_finder_spec.rb +++ b/spec/finders/boards/visits_finder_spec.rb @@ -10,7 +10,7 @@ describe Boards::VisitsFinder do let(:project) { create(:project) } let(:project_board) { create(:board, project: project) } - subject(:finder) { described_class.new(project_board.parent, user) } + subject(:finder) { described_class.new(project_board.resource_parent, user) } it 'returns nil when there is no user' do finder.current_user = nil @@ -27,7 +27,7 @@ describe Boards::VisitsFinder do it 'queries for last N visits' do expect(BoardProjectRecentVisit).to receive(:latest).with(user, project, count: 5).once - described_class.new(project_board.parent, user).latest(5) + described_class.new(project_board.resource_parent, user).latest(5) end end @@ -35,7 +35,7 @@ describe Boards::VisitsFinder do let(:group) { create(:group) } let(:group_board) { create(:board, group: group) } - subject(:finder) { described_class.new(group_board.parent, user) } + subject(:finder) { described_class.new(group_board.resource_parent, user) } it 'returns nil when there is no user' do finder.current_user = nil @@ -52,7 +52,7 @@ describe Boards::VisitsFinder do it 'queries for last N visits' do expect(BoardGroupRecentVisit).to receive(:latest).with(user, group, count: 5).once - described_class.new(group_board.parent, user).latest(5) + described_class.new(group_board.resource_parent, user).latest(5) end end end diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index 6576f3d1ff2..d1ab152330e 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -80,6 +80,81 @@ describe('Jobs Store Mutations', () => { expect(stateCopy.traceSize).toEqual(511846); expect(stateCopy.isTraceComplete).toEqual(true); }); + + describe('with new job log', () => { + let stateWithNewLog; + beforeEach(() => { + gon.features = gon.features || {}; + gon.features.jobLogJson = true; + + stateWithNewLog = state(); + }); + + afterEach(() => { + gon.features.jobLogJson = false; + }); + + describe('log.lines', () => { + describe('when append is true', () => { + it('sets the parsed log ', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { + append: true, + size: 511846, + complete: true, + lines: [ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + }, + ], + }); + + expect(stateWithNewLog.trace).toEqual([ + { + offset: 1, + content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }], + lineNumber: 0, + }, + ]); + }); + }); + + describe('when it is defined', () => { + it('sets the parsed log ', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { + append: false, + size: 511846, + complete: true, + lines: [ + { offset: 0, content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }] }, + ], + }); + + expect(stateWithNewLog.trace).toEqual([ + { + offset: 0, + content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }], + lineNumber: 0, + }, + ]); + }); + }); + + describe('when it is null', () => { + it('sets the default value', () => { + mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, { + append: true, + html, + size: 511846, + complete: false, + lines: null, + }); + + expect(stateWithNewLog.trace).toEqual([]); + }); + }); + }); + }); }); describe('STOP_POLLING_TRACE', () => { diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js index 9890e01460e..43dacfe622c 100644 --- a/spec/frontend/jobs/store/utils_spec.js +++ b/spec/frontend/jobs/store/utils_spec.js @@ -291,6 +291,13 @@ describe('Jobs Store Utils', () => { }); }); }); + + describe('when no data is provided', () => { + it('returns an empty array', () => { + const result = findOffsetAndRemove(); + expect(result).toEqual([]); + }); + }); }); describe('getIncrementalLineNumber', () => { diff --git a/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap new file mode 100644 index 00000000000..3f13b7d4d76 --- /dev/null +++ b/spec/frontend/registry/components/__snapshots__/group_empty_state_spec.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Registry Group Empty state to match the default snapshot 1`] = ` +<div + class="row container-message empty-state" +> + <div + class="col-12" + > + <div + class="svg-250 svg-content" + > + <img + alt="There are no container images available in this group" + class="" + src="imageUrl" + /> + </div> + </div> + + <div + class="col-12" + > + <div + class="text-content" + > + <h4 + class="center" + style="" + > + There are no container images available in this group + </h4> + + <p + class="center" + style="" + > + <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. + <a + href="help" + target="_blank" + > + More Information + </a> + </p> + </p> + + <div + class="text-center" + > + <!----> + + <!----> + </div> + </div> + </div> +</div> +`; diff --git a/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap new file mode 100644 index 00000000000..3084462f5ae --- /dev/null +++ b/spec/frontend/registry/components/__snapshots__/project_empty_state_spec.js.snap @@ -0,0 +1,186 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Registry Project Empty state to match the default snapshot 1`] = ` +<div + class="row container-message empty-state" +> + <div + class="col-12" + > + <div + class="svg-250 svg-content" + > + <img + alt="There are no container images stored for this project" + class="" + src="imageUrl" + /> + </div> + </div> + + <div + class="col-12" + > + <div + class="text-content" + > + <h4 + class="center" + style="" + > + There are no container images stored for this project + </h4> + + <p + class="center" + style="" + > + <p + class="js-no-container-images-text" + > + With the Container Registry, every project can have its own space to store its Docker images. + <a + href="help" + target="_blank" + > + More Information + </a> + </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 + <a + href="help_link" + target="_blank" + > + Two-Factor Authentication + </a> + enabled, use a + <a + href="personal_token" + target="_blank" + > + Personal Access Token + </a> + 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" + > + <button + class="btn input-group-text btn-secondary btn-default" + data-clipboard-text="docker login host" + data-original-title="Copy login command" + title="" + type="button" + > + <svg + aria-hidden="true" + class="s16 ic-duplicate" + > + <use + xlink:href="#duplicate" + /> + </svg> + </button> + </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" + > + <button + class="btn input-group-text btn-secondary btn-default" + data-clipboard-text="docker build -t url ." + data-original-title="Copy build command" + title="" + type="button" + > + <svg + aria-hidden="true" + class="s16 ic-duplicate" + > + <use + xlink:href="#duplicate" + /> + </svg> + </button> + </span> + </div> + + <div + class="input-group" + > + <input + class="form-control monospace" + readonly="readonly" + type="text" + /> + + <span + class="input-group-append" + > + <button + class="btn input-group-text btn-secondary btn-default" + data-clipboard-text="docker push url" + data-original-title="Copy push command" + title="" + type="button" + > + <svg + aria-hidden="true" + class="s16 ic-duplicate" + > + <use + xlink:href="#duplicate" + /> + </svg> + </button> + </span> + </div> + </p> + + <div + class="text-center" + > + <!----> + + <!----> + </div> + </div> + </div> +</div> +`; diff --git a/spec/frontend/registry/components/app_spec.js b/spec/frontend/registry/components/app_spec.js index 5dcb61e03b5..a69c33c246d 100644 --- a/spec/frontend/registry/components/app_spec.js +++ b/spec/frontend/registry/components/app_spec.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import { mount } from '@vue/test-utils'; import registry from '~/registry/components/app.vue'; import { TEST_HOST } from '../../helpers/test_constants'; @@ -7,8 +8,8 @@ describe('Registry List', () => { let wrapper; const findCollapsibleContainer = w => w.findAll({ name: 'CollapsibeContainerRegisty' }); - const findNoContainerImagesText = w => w.find('.js-no-container-images-text'); - const findNotLoggedInToRegistryText = w => w.find('.js-not-logged-in-to-registry-text'); + const findProjectEmptyState = w => w.find({ name: 'ProjectEmptyState' }); + const findGroupEmptyState = w => w.find({ name: 'GroupEmptyState' }); const findSpinner = w => w.find('.gl-spinner'); const findCharacterErrorText = w => w.find('.js-character-error-text'); @@ -25,13 +26,18 @@ describe('Registry List', () => { const setMainEndpoint = jest.fn(); const fetchRepos = jest.fn(); + const setIsDeleteDisabled = jest.fn(); const methods = { setMainEndpoint, fetchRepos, + setIsDeleteDisabled, }; beforeEach(() => { + // This is needed due to console.error called by vue to emit a warning that stop the tests. + // See https://github.com/vuejs/vue-test-utils/issues/532. + Vue.config.silent = true; wrapper = mount(registry, { propsData, computed: { @@ -43,6 +49,12 @@ describe('Registry List', () => { }); }); + afterEach(() => { + jest.clearAllMocks(); + Vue.config.silent = false; + wrapper.destroy(); + }); + describe('with data', () => { it('should render a list of CollapsibeContainerRegisty', () => { const containers = findCollapsibleContainer(wrapper); @@ -65,18 +77,9 @@ describe('Registry List', () => { }); }); - it('should render empty message', () => { - const noContainerImagesText = findNoContainerImagesText(localWrapper); - expect(noContainerImagesText.text()).toEqual( - 'With the Container Registry, every project can have its own space to store its Docker images. More Information', - ); - }); - - it('should render login help text', () => { - const notLoggedInToRegistryText = findNotLoggedInToRegistryText(localWrapper); - expect(notLoggedInToRegistryText.text()).toEqual( - '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 Two-Factor Authentication enabled, use a Personal Access Token instead of a password.', - ); + it('should render project empty message', () => { + const projectEmptyState = findProjectEmptyState(localWrapper); + expect(projectEmptyState.exists()).toBe(true); }); }); @@ -129,4 +132,29 @@ describe('Registry List', () => { ); }); }); + + describe('with groupId set', () => { + const isGroupPage = true; + + beforeEach(() => { + wrapper = mount(registry, { + propsData: { + ...propsData, + endpoint: null, + isGroupPage, + }, + methods, + }); + }); + + it('call the right vuex setters', () => { + expect(methods.setMainEndpoint).toHaveBeenLastCalledWith(null); + expect(methods.setIsDeleteDisabled).toHaveBeenLastCalledWith(true); + }); + + it('should render groups empty message', () => { + const groupEmptyState = findGroupEmptyState(wrapper); + expect(groupEmptyState.exists()).toBe(true); + }); + }); }); diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/components/collapsible_container_spec.js index 0fe4338f1ba..f93ebab1a4d 100644 --- a/spec/frontend/registry/components/collapsible_container_spec.js +++ b/spec/frontend/registry/components/collapsible_container_spec.js @@ -1,24 +1,40 @@ import Vue from 'vue'; -import { mount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { mount, createLocalVue } from '@vue/test-utils'; import collapsibleComponent from '~/registry/components/collapsible_container.vue'; import { repoPropsData } from '../mock_data'; import createFlash from '~/flash'; +import * as getters from '~/registry/stores/getters'; jest.mock('~/flash.js'); +const localVue = createLocalVue(); + +localVue.use(Vuex); + describe('collapsible registry container', () => { let wrapper; + let store; const findDeleteBtn = w => w.find('.js-remove-repo'); const findContainerImageTags = w => w.find('.container-image-tags'); const findToggleRepos = w => w.findAll('.js-toggle-repo'); + const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue }); + beforeEach(() => { createFlash.mockClear(); // This is needed due to console.error called by vue to emit a warning that stop the tests // see https://github.com/vuejs/vue-test-utils/issues/532 Vue.config.silent = true; - wrapper = mount(collapsibleComponent, { + store = new Vuex.Store({ + state: { + isDeleteDisabled: false, + }, + getters, + }); + + wrapper = mountWithStore({ propsData: { repo: repoPropsData, }, @@ -27,6 +43,7 @@ describe('collapsible registry container', () => { afterEach(() => { Vue.config.silent = false; + wrapper.destroy(); }); describe('toggle', () => { @@ -86,4 +103,25 @@ describe('collapsible registry container', () => { }); }); }); + + describe('disabled delete', () => { + beforeEach(() => { + store = new Vuex.Store({ + state: { + isDeleteDisabled: true, + }, + getters, + }); + wrapper = mountWithStore({ + propsData: { + repo: repoPropsData, + }, + }); + }); + + it('should not render delete button', () => { + const deleteBtn = findDeleteBtn(wrapper); + expect(deleteBtn.exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/registry/components/group_empty_state_spec.js b/spec/frontend/registry/components/group_empty_state_spec.js new file mode 100644 index 00000000000..f71074b5154 --- /dev/null +++ b/spec/frontend/registry/components/group_empty_state_spec.js @@ -0,0 +1,23 @@ +import { mount } from '@vue/test-utils'; +import groupEmptyState from '~/registry/components/group_empty_state.vue'; + +describe('Registry Group Empty state', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(groupEmptyState, { + propsData: { + noContainersImage: 'imageUrl', + helpPagePath: 'help', + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('to match the default snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/registry/components/project_empty_state_spec.js b/spec/frontend/registry/components/project_empty_state_spec.js new file mode 100644 index 00000000000..913524db3aa --- /dev/null +++ b/spec/frontend/registry/components/project_empty_state_spec.js @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils'; +import projectEmptyState from '~/registry/components/project_empty_state.vue'; + +describe('Registry Project Empty state', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(projectEmptyState, { + propsData: { + noContainersImage: 'imageUrl', + helpPagePath: 'help', + repositoryUrl: 'url', + twoFactorAuthHelpLink: 'help_link', + personalAccessTokensHelpLink: 'personal_token', + registryHostUrlWithPort: 'host', + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('to match the default snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js index 021f13feeba..600a7a6ee87 100644 --- a/spec/frontend/registry/components/table_registry_spec.js +++ b/spec/frontend/registry/components/table_registry_spec.js @@ -1,12 +1,19 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import tableRegistry from '~/registry/components/table_registry.vue'; -import { mount } from '@vue/test-utils'; +import { mount, createLocalVue } from '@vue/test-utils'; import { repoPropsData } from '../mock_data'; +import * as getters from '~/registry/stores/getters'; const [firstImage, secondImage] = repoPropsData.list; +const localVue = createLocalVue(); + +localVue.use(Vuex); + describe('table registry', () => { let wrapper; + let store; const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input'); const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input'); @@ -15,19 +22,31 @@ describe('table registry', () => { const findPagination = w => w.find('.js-registry-pagination'); const bulkDeletePath = 'path'; + const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue }); + beforeEach(() => { // This is needed due to console.error called by vue to emit a warning that stop the tests // see https://github.com/vuejs/vue-test-utils/issues/532 Vue.config.silent = true; - wrapper = mount(tableRegistry, { + + store = new Vuex.Store({ + state: { + isDeleteDisabled: false, + }, + getters, + }); + + wrapper = mountWithStore({ propsData: { repo: repoPropsData, + canDeleteRepo: true, }, }); }); afterEach(() => { Vue.config.silent = false; + wrapper.destroy(); }); describe('rendering', () => { @@ -149,7 +168,6 @@ describe('table registry', () => { }); describe('pagination', () => { - let localWrapper = null; const repo = { repoPropsData, pagination: { @@ -160,7 +178,7 @@ describe('table registry', () => { }; beforeEach(() => { - localWrapper = mount(tableRegistry, { + wrapper = mount(tableRegistry, { propsData: { repo, }, @@ -168,13 +186,13 @@ describe('table registry', () => { }); it('should exist', () => { - const pagination = findPagination(localWrapper); + const pagination = findPagination(wrapper); expect(pagination.exists()).toBe(true); }); it('should be visible when pagination is needed', () => { - const pagination = findPagination(localWrapper); + const pagination = findPagination(wrapper); expect(pagination.isVisible()).toBe(true); - localWrapper.setProps({ + wrapper.setProps({ repo: { pagination: { total: 0, @@ -182,13 +200,13 @@ describe('table registry', () => { }, }, }); - expect(localWrapper.vm.shouldRenderPagination).toBe(false); + expect(wrapper.vm.shouldRenderPagination).toBe(false); }); it('should have a change function that update the list when run', () => { const fetchList = jest.fn().mockResolvedValue(); - localWrapper.setMethods({ fetchList }); - localWrapper.vm.onPageChange(1); - expect(localWrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 }); + wrapper.setMethods({ fetchList }); + wrapper.vm.onPageChange(1); + expect(wrapper.vm.fetchList).toHaveBeenCalledWith({ repo, page: 1 }); }); }); @@ -208,4 +226,41 @@ describe('table registry', () => { expect(wrapper.vm.modalDescription).toContain('<b>2</b> tags'); }); }); + + describe('disabled delete', () => { + beforeEach(() => { + store = new Vuex.Store({ + state: { + isDeleteDisabled: true, + }, + getters, + }); + wrapper = mountWithStore({ + propsData: { + repo: repoPropsData, + canDeleteRepo: false, + }, + }); + }); + + it('should not render select all', () => { + const selectAll = findSelectAllCheckbox(wrapper); + expect(selectAll.exists()).toBe(false); + }); + + it('should not render any select checkbox', () => { + const selects = findSelectCheckboxes(wrapper); + expect(selects.length).toBe(0); + }); + + it('should not render delete registry button', () => { + const deleteBtn = findDeleteButton(wrapper); + expect(deleteBtn.exists()).toBe(false); + }); + + it('should not render delete row button', () => { + const deleteBtns = findDeleteButtonsRow(wrapper); + expect(deleteBtns.length).toBe(0); + }); + }); }); diff --git a/spec/frontend/registry/stores/actions_spec.js b/spec/frontend/registry/stores/actions_spec.js index bf335904d23..7937fa82e80 100644 --- a/spec/frontend/registry/stores/actions_spec.js +++ b/spec/frontend/registry/stores/actions_spec.js @@ -34,7 +34,7 @@ describe('Actions Registry Store', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {}); }); - it('should set receveived repos', done => { + it('should set received repos', done => { testAction( actions.fetchRepos, null, @@ -71,10 +71,10 @@ describe('Actions Registry Store', () => { beforeEach(() => { state.repos = parsedReposServerResponse; [, repo] = state.repos; - mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {}); }); it('should set received list', done => { + mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {}); testAction( actions.fetchList, { repo }, @@ -97,6 +97,7 @@ describe('Actions Registry Store', () => { }); it('should create flash on API error', done => { + mock.onGet(repo.tagsPath).replyOnce(400); const updatedRepo = { ...repo, tagsPath: null, @@ -133,6 +134,19 @@ describe('Actions Registry Store', () => { }); }); + describe('setIsDeleteDisabled', () => { + it('should commit set is delete disabled', done => { + testAction( + actions.setIsDeleteDisabled, + true, + state, + [{ type: types.SET_IS_DELETE_DISABLED, payload: true }], + [], + done, + ); + }); + }); + describe('toggleLoading', () => { it('should commit toggle main loading', done => { testAction( diff --git a/spec/frontend/registry/stores/getters_spec.js b/spec/frontend/registry/stores/getters_spec.js index 839aa718997..c16f520223b 100644 --- a/spec/frontend/registry/stores/getters_spec.js +++ b/spec/frontend/registry/stores/getters_spec.js @@ -7,6 +7,7 @@ describe('Getters Registry Store', () => { state = { isLoading: false, endpoint: '/root/empty-project/container_registry.json', + isDeleteDisabled: false, repos: [ { canDelete: true, @@ -43,4 +44,9 @@ describe('Getters Registry Store', () => { expect(getters.repos(state)).toEqual(state.repos); }); }); + describe('isDeleteDisabled', () => { + it('should return isDeleteDisabled', () => { + expect(getters.isDeleteDisabled(state)).toEqual(state.isDeleteDisabled); + }); + }); }); diff --git a/spec/frontend/registry/stores/mutations_spec.js b/spec/frontend/registry/stores/mutations_spec.js index e19fe7a27cf..1d583028ca6 100644 --- a/spec/frontend/registry/stores/mutations_spec.js +++ b/spec/frontend/registry/stores/mutations_spec.js @@ -19,7 +19,16 @@ describe('Mutations Registry Store', () => { const expectedState = Object.assign({}, mockState, { endpoint: 'foo' }); mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo'); - expect(mockState).toEqual(expectedState); + expect(mockState.endpoint).toEqual(expectedState.endpoint); + }); + }); + + describe('SET_IS_DELETE_DISABLED', () => { + it('should set the is delete disabled', () => { + const expectedState = Object.assign({}, mockState, { isDeleteDisabled: true }); + mutations[types.SET_IS_DELETE_DISABLED](mockState, true); + + expect(mockState.isDeleteDisabled).toEqual(expectedState.isDeleteDisabled); }); }); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 98719697cea..8b33277ea18 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -191,6 +191,41 @@ describe GroupsHelper do end end + describe '#group_container_registry_nav' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + before do + stub_container_registry_config(enabled: true) + allow(helper).to receive(:current_user) { user } + allow(helper).to receive(:can?).with(user, :read_container_image, group) { true } + helper.instance_variable_set(:@group, group) + end + + subject { helper.group_container_registry_nav? } + + context 'when container registry is enabled' do + it { is_expected.to be_truthy } + + it 'is disabled for guest' do + allow(helper).to receive(:can?).with(user, :read_container_image, group) { false } + expect(subject).to be false + end + end + + context 'when container registry is not enabled' do + before do + stub_container_registry_config(enabled: false) + end + + it { is_expected.to be_falsy } + + it 'is disabled for guests' do + allow(helper).to receive(:can?).with(user, :read_container_image, group) { false } + expect(subject).to be false + end + end + end + describe '#group_sidebar_links' do let(:group) { create(:group, :public) } let(:user) { create(:user) } diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb new file mode 100644 index 00000000000..1be279375bd --- /dev/null +++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::MentionedUserParser do + include ReferenceParserHelpers + + let(:group) { create(:group, :private) } + let(:user) { create(:user) } + let(:new_user) { create(:user) } + let(:project) { create(:project, group: group, creator: user) } + let(:link) { empty_html_link } + + subject { described_class.new(Banzai::RenderContext.new(project, new_user)) } + + describe '#gather_references' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID' do + before do + link['data-group'] = project.group.id.to_s + group.add_developer(new_user) + end + + it 'returns empty list of users' do + expect(subject.gather_references([link])).to eq([]) + end + end + end + + context 'when the link has a data-project attribute' do + context 'using an existing project ID' do + before do + link['data-project'] = project.id.to_s + project.add_developer(new_user) + end + + it 'returns empty list of users' do + expect(subject.gather_references([link])).to eq([]) + end + end + end + + context 'when the link has a data-user attribute' do + it 'returns an Array of users' do + link['data-user'] = user.id.to_s + + expect(subject.referenced_by([link])).to eq([user]) + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb new file mode 100644 index 00000000000..99d607629eb --- /dev/null +++ b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::MentionedUsersByGroupParser do + include ReferenceParserHelpers + + let(:group) { create(:group, :private) } + let(:user) { create(:user) } + let(:new_user) { create(:user) } + let(:project) { create(:project, group: group, creator: user) } + let(:link) { empty_html_link } + + subject { described_class.new(Banzai::RenderContext.new(project, new_user)) } + + describe '#gather_references' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID where user does not have access' do + it 'returns empty array' do + link['data-group'] = project.group.id.to_s + + expect(subject.gather_references([link])).to eq([]) + end + end + + context 'using an existing group ID' do + before do + link['data-group'] = project.group.id.to_s + group.add_developer(new_user) + end + + it 'returns groups' do + expect(subject.gather_references([link])).to eq([group]) + end + end + + context 'using a non-existing group ID' do + it 'returns an empty Array' do + link['data-group'] = 'test-non-existing' + + expect(subject.gather_references([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb new file mode 100644 index 00000000000..155f2189d9e --- /dev/null +++ b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::MentionedUsersByProjectParser do + include ReferenceParserHelpers + + let(:group) { create(:group, :private) } + let(:user) { create(:user) } + let(:new_user) { create(:user) } + let(:project) { create(:project, group: group, creator: user) } + let(:link) { empty_html_link } + + subject { described_class.new(Banzai::RenderContext.new(project, new_user)) } + + describe '#gather_references' do + context 'when the link has a data-project attribute' do + context 'using an existing project ID where user does not have access' do + it 'returns empty Array' do + link['data-project'] = project.id.to_s + + expect(subject.gather_references([link])).to eq([]) + end + end + + context 'using an existing project ID' do + before do + link['data-project'] = project.id.to_s + project.add_developer(new_user) + end + + it 'returns an Array of referenced projects' do + expect(subject.gather_references([link])).to eq([project]) + end + end + + context 'using a non-existing project ID' do + it 'returns an empty Array' do + link['data-project'] = 'inexisting-project-id' + + expect(subject.gather_references([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb new file mode 100644 index 00000000000..042f9b591b6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/edge_stages_injector_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Ci::Config::EdgeStagesInjector do + describe '#call' do + subject { described_class.new(config).to_hash } + + context 'without stages' do + let(:config) do + { + test: { script: 'test' } + } + end + + it { is_expected.to match config } + end + + context 'with values' do + let(:config) do + { + stages: %w[stage1 stage2], + test: { script: 'test' } + } + end + + let(:expected_stages) do + %w[.pre stage1 stage2 .post] + end + + it { is_expected.to match(config.merge(stages: expected_stages)) } + end + + context 'with bad values' do + let(:config) do + { + stages: 'stage1', + test: { script: 'test' } + } + end + + it { is_expected.to match(config) } + end + + context 'with collision values' do + let(:config) do + { + stages: %w[.post stage1 .pre .post stage2], + test: { script: 'test' } + } + end + + let(:expected_stages) do + %w[.pre stage1 stage2 .post] + end + + it { is_expected.to match(config.merge(stages: expected_stages)) } + end + + context 'with types' do + let(:config) do + { + types: %w[stage1 stage2], + test: { script: 'test' } + } + end + + let(:expected_config) do + { + types: %w[.pre stage1 stage2 .post], + test: { script: 'test' } + } + end + + it { is_expected.to match expected_config } + end + + context 'with types' do + let(:config) do + { + types: %w[.post stage1 .pre .post stage2], + test: { script: 'test' } + } + end + + let(:expected_config) do + { + types: %w[.pre stage1 stage2 .post], + test: { script: 'test' } + } + end + + it { is_expected.to match expected_config } + end + end + + describe '.wrap_stages' do + subject { described_class.wrap_stages(stages) } + + context 'with empty value' do + let(:stages) {} + + it { is_expected.to eq %w[.pre .post] } + end + + context 'with values' do + let(:stages) { %w[s1 .pre] } + + it { is_expected.to eq %w[.pre s1 .post] } + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 968dbb9c7f2..7e1a80414d4 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -215,7 +215,7 @@ describe Gitlab::Ci::Config::Entry::Root do describe '#stages_value' do it 'returns an array of root stages' do - expect(root.stages_value).to eq %w[build test deploy] + expect(root.stages_value).to eq %w[.pre build test deploy .post] end end diff --git a/spec/lib/gitlab/ci/config/entry/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb index 97970522104..3e6ff8eca28 100644 --- a/spec/lib/gitlab/ci/config/entry/stages_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Stages do describe '.default' do it 'returns default stages' do - expect(described_class.default).to eq %w[build test deploy] + expect(described_class.default).to eq %w[.pre build test deploy .post] end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 68c38644b5c..b254f9af2f1 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -51,6 +51,54 @@ describe Gitlab::Ci::Config do end end end + + describe '#stages' do + subject(:subject) { config.stages } + + context 'with default stages' do + let(:default_stages) do + %w[.pre build test deploy .post] + end + + it { is_expected.to eq default_stages } + end + + context 'with custom stages' do + let(:yml) do + <<-EOS + stages: + - stage1 + - stage2 + job1: + stage: stage1 + script: + - ls + EOS + end + + it { is_expected.to eq %w[.pre stage1 stage2 .post] } + end + + context 'with feature disabled' do + before do + stub_feature_flags(ci_pre_post_pipeline_stages: false) + end + + let(:yml) do + <<-EOS + stages: + - stage1 + - stage2 + job1: + stage: stage1 + script: + - ls + EOS + end + + it { is_expected.to eq %w[stage1 stage2] } + end + end end context 'when using extendable hash' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index d43eb4e4b4a..cb5ebde16d7 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -26,7 +26,7 @@ module Gitlab it 'returns valid build attributes' do expect(subject).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -56,7 +56,7 @@ module Gitlab it 'returns valid build attributes' do expect(subject).to eq({ stage: 'test', - stage_idx: 1, + stage_idx: 2, name: 'rspec', options: { script: ['rspec'] }, rules: [ @@ -209,13 +209,16 @@ module Gitlab end let(:attributes) do - [{ name: "build", + [{ name: ".pre", index: 0, builds: [] }, - { name: "test", + { name: "build", index: 1, + builds: [] }, + { name: "test", + index: 2, builds: - [{ stage_idx: 1, + [{ stage_idx: 2, stage: "test", name: "rspec", allow_failure: false, @@ -225,9 +228,9 @@ module Gitlab only: { refs: ["branches"] }, except: {} }] }, { name: "deploy", - index: 2, + index: 3, builds: - [{ stage_idx: 2, + [{ stage_idx: 3, stage: "deploy", name: "prod", allow_failure: false, @@ -235,7 +238,10 @@ module Gitlab yaml_variables: [], options: { script: ["cap prod"] }, only: { refs: ["tags"] }, - except: {} }] }] + except: {} }] }, + { name: ".post", + index: 4, + builds: [] }] end it 'returns stages seed attributes' do @@ -425,7 +431,7 @@ module Gitlab expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -456,7 +462,7 @@ module Gitlab expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -485,7 +491,7 @@ module Gitlab expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -510,7 +516,7 @@ module Gitlab expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -977,7 +983,7 @@ module Gitlab expect(config_processor.stage_builds_attributes("test").size).to eq(1) expect(config_processor.stage_builds_attributes("test").first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "rspec", options: { before_script: ["pwd"], @@ -1272,7 +1278,7 @@ module Gitlab expect(subject.builds.size).to eq(5) expect(subject.builds[0]).to eq( stage: "build", - stage_idx: 0, + stage_idx: 1, name: "build1", options: { script: ["test"] @@ -1283,7 +1289,7 @@ module Gitlab ) expect(subject.builds[2]).to eq( stage: "test", - stage_idx: 1, + stage_idx: 2, name: "test1", options: { script: ["test"], @@ -1398,7 +1404,7 @@ module Gitlab expect(subject.size).to eq(1) expect(subject.first).to eq({ stage: "test", - stage_idx: 1, + stage_idx: 2, name: "normal_job", options: { script: ["test"] @@ -1442,7 +1448,7 @@ module Gitlab expect(subject.size).to eq(2) expect(subject.first).to eq({ stage: "build", - stage_idx: 0, + stage_idx: 1, name: "job1", options: { script: ["execute-script-for-job"] @@ -1453,7 +1459,7 @@ module Gitlab }) expect(subject.second).to eq({ stage: "build", - stage_idx: 0, + stage_idx: 1, name: "job2", options: { script: ["execute-script-for-job"] @@ -1665,14 +1671,14 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post") end it "returns errors if stages is not an array" do diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index e8a9f0b06a8..58509b69463 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -90,4 +90,12 @@ describe Gitlab::DataBuilder::Push do .not_to raise_error end end + + describe '.build_bulk' do + subject do + described_class.build_bulk(action: :created, ref_type: :branch, changes: [double, double]) + end + + it { is_expected.to eq(action: :created, ref_count: 2, ref_type: :branch) } + end end diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 6d614c6527a..8331f0b6bc7 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -311,10 +311,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi end end - it 'creates the merge request diffs' do + it 'creates a merge request diff and sets it as the latest' do mr = insert_git_data expect(mr.merge_request_diffs.exists?).to eq(true) + expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id) end it 'creates the merge request diff commits' do diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index fa47cfd519b..8401b683fd5 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -370,5 +370,33 @@ describe Gitlab::Gpg::Commit do it_behaves_like 'returns the cached signature on second call' end + + context 'multiple commits with signatures' do + let(:first_signature) { create(:gpg_signature) } + + let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) } + let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) } + + let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) } + let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) } + + let(:commits) do + [first_commit, second_commit].map do |commit| + gpg_commit = described_class.new(commit) + + allow(gpg_commit).to receive(:has_signature?).and_return(true) + + gpg_commit + end + end + + it 'does an aggregated sql request instead of 2 separate ones' do + recorder = ActiveRecord::QueryRecorder.new do + commits.each(&:signature) + end + + expect(recorder.count).to eq(1) + end + end end end diff --git a/spec/lib/gitlab/import/merge_request_creator_spec.rb b/spec/lib/gitlab/import/merge_request_creator_spec.rb index 7c73e9b39f7..ff2c3032dbf 100644 --- a/spec/lib/gitlab/import/merge_request_creator_spec.rb +++ b/spec/lib/gitlab/import/merge_request_creator_spec.rb @@ -21,8 +21,11 @@ describe Gitlab::Import::MergeRequestCreator do subject.execute(attributes) - expect(merge_request.reload.merge_request_diffs.count).to eq(1) - expect(merge_request.reload.merge_request_diffs.first.commits.count).to eq(commits_count) + merge_request.reload + + expect(merge_request.merge_request_diffs.count).to eq(1) + expect(merge_request.merge_request_diffs.first.commits.count).to eq(commits_count) + expect(merge_request.latest_merge_request_diff_id).to eq(merge_request.merge_request_diffs.first.id) end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d3b51a53ede..ebc5d9d1f56 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -47,6 +47,7 @@ PushEventPayload: - commit_to - ref - commit_title +- ref_count Note: - id - note diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb new file mode 100644 index 00000000000..a415b6407d5 --- /dev/null +++ b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Metrics::Exporter::SidekiqExporter do + let(:exporter) { described_class.new } + + after do + exporter.stop + end + + context 'with valid config' do + before do + stub_config( + monitoring: { + sidekiq_exporter: { + enabled: true, + port: 0, + address: '127.0.0.1' + } + } + ) + end + + it 'does start thread' do + expect(exporter.start).not_to be_nil + end + end + + context 'when port is already taken' do + let(:first_exporter) { described_class.new } + + before do + stub_config( + monitoring: { + sidekiq_exporter: { + enabled: true, + port: 9992, + address: '127.0.0.1' + } + } + ) + + first_exporter.start + end + + after do + first_exporter.stop + end + + it 'does print error message' do + expect(Sidekiq.logger).to receive(:error) + .with( + class: described_class.to_s, + message: 'Cannot start sidekiq_exporter', + exception: anything) + + exporter.start + end + + it 'does not start thread' do + expect(exporter.start).to be_nil + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7513dbeeb6f..f6ace0d8bf5 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -265,7 +265,8 @@ describe Gitlab::ReferenceExtractor do describe 'referables prefixes' do def prefixes described_class::REFERABLES.each_with_object({}) do |referable, result| - klass = referable.to_s.camelize.constantize + class_name = referable.to_s.camelize + klass = class_name.constantize if Object.const_defined?(class_name) next unless klass.respond_to?(:reference_prefix) diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 702a6fab0e6..7bef3d30064 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -60,6 +60,10 @@ describe ApplicationSetting do it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) } it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) } + it { is_expected.to allow_value(3).for(:push_event_activities_limit) } + it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) } + it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } + context "when user accepted let's encrypt terms of service" do before do setting.update(lets_encrypt_terms_of_service_accepted: true) diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index 4911375c962..dd18c8842ab 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -60,6 +60,18 @@ RSpec.describe GpgSignature do end end + describe '.by_commit_sha scope' do + let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) } + let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) } + + it 'returns all gpg signatures by sha' do + expect(described_class.by_commit_sha(commit_sha)).to eq([gpg_signature]) + expect( + described_class.by_commit_sha([commit_sha, another_gpg_signature.commit_sha]) + ).to contain_exactly(gpg_signature, another_gpg_signature) + end + end + describe '#commit' do it 'fetches the commit through the project' do expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit) diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 83c7464757f..8a47b8c206b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -972,13 +972,13 @@ describe Note do project = create(:project) note = create(:note_on_issue, project: project) - expect(note.parent).to eq(project) + expect(note.resource_parent).to eq(project) end it 'returns nil for personal snippet note' do note = create(:note_on_personal_snippet) - expect(note.parent).to be_nil + expect(note.resource_parent).to be_nil end end diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 2fc772b12af..992fd5e9c66 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -122,6 +122,7 @@ describe API::Events do expect(payload_hash['action']).to eq(payload.action) expect(payload_hash['ref_type']).to eq(payload.ref_type) expect(payload_hash['commit_to']).to eq(payload.commit_to) + expect(payload_hash['ref_count']).to eq(payload.ref_count) end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index af1cf80e9d3..f3bfb258029 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -73,7 +73,8 @@ describe API::Settings, 'Settings' do local_markdown_version: 3, allow_local_requests_from_web_hooks_and_services: true, allow_local_requests_from_system_hooks: false, - push_event_hooks_limit: 2 + push_event_hooks_limit: 2, + push_event_activities_limit: 2 } expect(response).to have_gitlab_http_status(200) @@ -104,6 +105,7 @@ describe API::Settings, 'Settings' do expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true) expect(json_response['allow_local_requests_from_system_hooks']).to eq(false) expect(json_response['push_event_hooks_limit']).to eq(2) + expect(json_response['push_event_activities_limit']).to eq(2) end end diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb index 5848dd64c3b..799a8d5c122 100644 --- a/spec/serializers/container_repository_entity_spec.rb +++ b/spec/serializers/container_repository_entity_spec.rb @@ -25,6 +25,18 @@ describe ContainerRepositoryEntity do expect(subject).to include(:id, :path, :location, :tags_path) end + context 'when project is not preset in the request' do + before do + allow(request).to receive(:respond_to?).and_return(false) + allow(request).to receive(:project).and_return(nil) + end + + it 'uses project from the object' do + expect(request.project).not_to equal(project) + expect(subject).to include(:tags_path) + end + end + context 'when user can manage repositories' do before do project.add_developer(user) diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb index 33637419f83..ef7b7fdbaac 100644 --- a/spec/services/boards/issues/create_service_spec.rb +++ b/spec/services/boards/issues/create_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Issues::CreateService do let(:label) { create(:label, project: project, name: 'in-progress') } let!(:list) { create(:list, board: board, label: label, position: 0) } - subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') } + subject(:service) { described_class.new(board.resource_parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') } before do project.add_developer(user) diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb index a5411a2fb3a..243e0fc50ad 100644 --- a/spec/services/boards/lists/update_service_spec.rb +++ b/spec/services/boards/lists/update_service_spec.rb @@ -9,9 +9,9 @@ describe Boards::Lists::UpdateService do shared_examples 'moving list' do context 'when user can admin list' do it 'calls Lists::MoveService to update list position' do - board.parent.add_developer(user) + board.resource_parent.add_developer(user) - expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, params).and_call_original + expect(Boards::Lists::MoveService).to receive(:new).with(board.resource_parent, user, params).and_call_original expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list) service.execute(list) @@ -30,7 +30,7 @@ describe Boards::Lists::UpdateService do shared_examples 'updating list preferences' do context 'when user can read list' do it 'updates list preference for user' do - board.parent.add_guest(user) + board.resource_parent.add_guest(user) service.execute(list) @@ -48,7 +48,7 @@ describe Boards::Lists::UpdateService do end describe '#execute' do - let(:service) { described_class.new(board.parent, user, params) } + let(:service) { described_class.new(board.resource_parent, user, params) } context 'when position parameter is present' do let(:params) { { position: 1 } } diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb index 6baf7ac9deb..203c287f396 100644 --- a/spec/services/boards/visits/create_service_spec.rb +++ b/spec/services/boards/visits/create_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Visits::CreateService do let(:project) { create(:project) } let(:project_board) { create(:board, project: project) } - subject(:service) { described_class.new(project_board.parent, user) } + subject(:service) { described_class.new(project_board.resource_parent, user) } it 'returns nil when there is no user' do service.current_user = nil @@ -35,7 +35,7 @@ describe Boards::Visits::CreateService do let(:group) { create(:group) } let(:group_board) { create(:board, group: group) } - subject(:service) { described_class.new(group_board.parent, user) } + subject(:service) { described_class.new(group_board.resource_parent, user) } it 'returns nil when there is no user' do service.current_user = nil diff --git a/spec/services/bulk_push_event_payload_service_spec.rb b/spec/services/bulk_push_event_payload_service_spec.rb new file mode 100644 index 00000000000..661c3540aa0 --- /dev/null +++ b/spec/services/bulk_push_event_payload_service_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BulkPushEventPayloadService do + let(:event) { create(:push_event) } + + let(:push_data) do + { + action: :created, + ref_count: 4, + ref_type: :branch + } + end + + subject { described_class.new(event, push_data) } + + it 'creates a PushEventPayload' do + push_event_payload = subject.execute + + expect(push_event_payload).to be_persisted + expect(push_event_payload.action).to eq(push_data[:action].to_s) + expect(push_event_payload.commit_count).to eq(0) + expect(push_event_payload.ref_count).to eq(push_data[:ref_count]) + expect(push_event_payload.ref_type).to eq(push_data[:ref_type].to_s) + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 9f2c3fec62c..eb738ac80b1 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -113,40 +113,21 @@ describe EventCreateService do end end - describe '#push', :clean_gitlab_redis_shared_state do - let(:project) { create(:project) } - let(:user) { create(:user) } - - let(:push_data) do - { - commits: [ - { - id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', - message: 'This is a commit' - } - ], - before: '0000000000000000000000000000000000000000', - after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', - total_commits_count: 1, - ref: 'refs/heads/my-branch' - } - end - + shared_examples_for 'service for creating a push event' do |service_class| it 'creates a new event' do - expect { service.push(project, user, push_data) }.to change { Event.count } + expect { subject }.to change { Event.count } end it 'creates the push event payload' do - expect(PushEventPayloadService).to receive(:new) + expect(service_class).to receive(:new) .with(an_instance_of(PushEvent), push_data) .and_call_original - service.push(project, user, push_data) + subject end it 'updates user last activity' do - expect { service.push(project, user, push_data) } - .to change { user.last_activity_on }.to(Date.today) + expect { subject }.to change { user.last_activity_on }.to(Date.today) end it 'caches the last push event for the user' do @@ -154,7 +135,7 @@ describe EventCreateService do .to receive(:cache_last_push_event) .with(an_instance_of(PushEvent)) - service.push(project, user, push_data) + subject end it 'does not create any event data when an error is raised' do @@ -163,17 +144,56 @@ describe EventCreateService do allow(payload_service).to receive(:execute) .and_raise(RuntimeError) - allow(PushEventPayloadService).to receive(:new) + allow(service_class).to receive(:new) .and_return(payload_service) - expect { service.push(project, user, push_data) } - .to raise_error(RuntimeError) - + expect { subject }.to raise_error(RuntimeError) expect(Event.count).to eq(0) expect(PushEventPayload.count).to eq(0) end end + describe '#push', :clean_gitlab_redis_shared_state do + let(:project) { create(:project) } + let(:user) { create(:user) } + + let(:push_data) do + { + commits: [ + { + id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + message: 'This is a commit' + } + ], + before: '0000000000000000000000000000000000000000', + after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + total_commits_count: 1, + ref: 'refs/heads/my-branch' + } + end + + subject { service.push(project, user, push_data) } + + it_behaves_like 'service for creating a push event', PushEventPayloadService + end + + describe '#bulk_push', :clean_gitlab_redis_shared_state do + let(:project) { create(:project) } + let(:user) { create(:user) } + + let(:push_data) do + { + action: :created, + ref_count: 4, + ref_type: :branch + } + end + + subject { service.bulk_push(project, user, push_data) } + + it_behaves_like 'service for creating a push event', BulkPushEventPayloadService + end + describe 'Project' do let(:user) { create :user } let(:project) { create(:project) } diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb index 90b3eb38469..f3f6b36a18d 100644 --- a/spec/services/git/base_hooks_service_spec.rb +++ b/spec/services/git/base_hooks_service_spec.rb @@ -12,8 +12,8 @@ describe Git::BaseHooksService do let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 let(:ref) { 'refs/tags/v1.1.0' } - describe '#execute_project_hooks' do - class TestService < described_class + let(:test_service) do + Class.new(described_class) do def hook_name :push_hooks end @@ -22,22 +22,44 @@ describe Git::BaseHooksService do [] end end + end - let(:project) { create(:project, :repository) } + subject { test_service.new(project, user, params) } - let(:params) do - { - change: { - oldrev: oldrev, - newrev: newrev, - ref: ref - } + let(:params) do + { + change: { + oldrev: oldrev, + newrev: newrev, + ref: ref } + } + end + + describe 'push event' do + it 'creates push event' do + expect_next_instance_of(EventCreateService) do |service| + expect(service).to receive(:push) + end + + subject.execute end - subject { TestService.new(project, user, params) } + context 'create_push_event is set to false' do + before do + params[:create_push_event] = false + end + + it 'does not create push event' do + expect(EventCreateService).not_to receive(:new) + + subject.execute + end + end + end - context '#execute_hooks' do + describe 'project hooks and services' do + context 'hooks' do before do expect(project).to receive(:has_active_hooks?).and_return(active) end @@ -65,7 +87,7 @@ describe Git::BaseHooksService do end end - context '#execute_services' do + context 'services' do before do expect(project).to receive(:has_active_services?).and_return(active) end diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb index eeb395f6c7b..35ddf95b5f6 100644 --- a/spec/services/git/process_ref_changes_service_spec.rb +++ b/spec/services/git/process_ref_changes_service_spec.rb @@ -13,6 +13,12 @@ describe Git::ProcessRefChangesService do let(:service) { double(execute: true) } let(:git_changes) { double(branch_changes: [], tag_changes: []) } + def multiple_changes(change, count) + Array.new(count).map.with_index do |n, index| + { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" } + end + end + let(:changes) do [ { index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" }, @@ -28,7 +34,7 @@ describe Git::ProcessRefChangesService do it "calls #{push_service_class}" do expect(push_service_class) .to receive(:new) - .with(project, project.owner, hash_including(execute_project_hooks: true)) + .with(project, project.owner, hash_including(execute_project_hooks: true, create_push_event: true)) .exactly(changes.count).times .and_return(service) @@ -36,12 +42,6 @@ describe Git::ProcessRefChangesService do end context 'changes exceed push_event_hooks_limit' do - def multiple_changes(change, count) - Array.new(count).map.with_index do |n, index| - { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" } - end - end - let(:push_event_hooks_limit) { 3 } let(:changes) do @@ -88,6 +88,40 @@ describe Git::ProcessRefChangesService do end end + context 'changes exceed push_event_activities_limit per action' do + let(:push_event_activities_limit) { 3 } + + let(:changes) do + [ + { oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" }, + { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" }, + { oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" } + ].map do |change| + multiple_changes(change, push_event_activities_limit + 1) + end.flatten + end + + before do + stub_application_setting(push_event_activities_limit: push_event_activities_limit) + end + + it "calls #{push_service_class} with create_push_event set to false" do + expect(push_service_class) + .to receive(:new) + .with(project, project.owner, hash_including(create_push_event: false)) + .exactly(changes.count).times + .and_return(service) + + subject.execute + end + + it 'creates events per action' do + allow(push_service_class).to receive(:new).and_return(service) + + expect { subject.execute }.to change { Event.count }.by(3) + end + end + context 'pipeline creation' do context 'with valid .gitlab-ci.yml' do before do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index ec68e1a8cf9..788f83cc233 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -1545,12 +1545,20 @@ describe QuickActions::InterpretService do end it 'limits to commands passed ' do - content = "/shrug\n/close" + content = "/shrug test\n/close" text, commands = service.execute(content, issue, only: [:shrug]) expect(commands).to be_empty - expect(text).to eq("#{described_class::SHRUG}\n/close") + expect(text).to eq("test #{described_class::SHRUG}\n/close") + end + + it 'preserves leading whitespace ' do + content = " - list\n\n/close\n\ntest\n\n" + + text, _ = service.execute(content, issue) + + expect(text).to eq(" - list\n\ntest") end context '/create_merge_request command' do diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb index b7aff32460d..d41490f33e4 100644 --- a/spec/support/api/boards_shared_examples.rb +++ b/spec/support/api/boards_shared_examples.rb @@ -171,7 +171,7 @@ shared_examples_for 'group and project boards' do |route_definition, ee = false| if board_parent.try(:namespace) board_parent.update(namespace: owner.namespace) else - board.parent.add_owner(owner) + board.resource_parent.add_owner(owner) end end diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index d6439f77408..ce8c2140e99 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -205,7 +205,7 @@ shared_examples_for 'group and project milestones' do |route_definition| describe "DELETE #{route_definition}/:milestone_id" do it "rejects a member with reporter access from deleting a milestone" do reporter = create(:user) - milestone.parent.add_reporter(reporter) + milestone.resource_parent.add_reporter(reporter) delete api(resource_route, reporter) diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb index 19818a6091b..7fd69354c2d 100644 --- a/spec/support/shared_examples/services/boards/boards_create_service.rb +++ b/spec/support/shared_examples/services/boards/boards_create_service.rb @@ -17,7 +17,7 @@ shared_examples 'boards create service' do context 'when parent has a board' do before do - create(:board, parent: parent) + create(:board, resource_parent: parent) end it 'does not create a new board' do diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb index 566e5050f8e..25dc2e04942 100644 --- a/spec/support/shared_examples/services/boards/boards_list_service.rb +++ b/spec/support/shared_examples/services/boards/boards_list_service.rb @@ -15,7 +15,7 @@ shared_examples 'boards list service' do context 'when parent has a board' do before do - create(:board, parent: parent) + create(:board, resource_parent: parent) end it 'does not create a new board' do @@ -24,7 +24,7 @@ shared_examples 'boards list service' do end it 'returns parent boards' do - board = create(:board, parent: parent) + board = create(:board, resource_parent: parent) expect(service.execute).to eq [board] end diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb index e43e37188a3..d33a8aa86fc 100644 --- a/spec/views/events/event/_push.html.haml_spec.rb +++ b/spec/views/events/event/_push.html.haml_spec.rb @@ -28,6 +28,23 @@ describe 'events/event/_push.html.haml' do expect(rendered).not_to have_link(event.ref_name) end end + + context 'ref_count is more than 1' do + let(:payload) do + build_stubbed( + :push_event_payload, + event: event, + ref_count: 4, + ref_type: :branch + ) + end + + it 'includes the count in the text' do + render partial: 'events/event/push', locals: { event: event } + + expect(rendered).to include('4 branches') + end + end end context 'with a tag' do @@ -53,5 +70,22 @@ describe 'events/event/_push.html.haml' do expect(rendered).not_to have_link(event.ref_name) end end + + context 'ref_count is more than 1' do + let(:payload) do + build_stubbed( + :push_event_payload, + event: event, + ref_count: 4, + ref_type: :tag + ) + end + + it 'includes the count in the text' do + render partial: 'events/event/push', locals: { event: event } + + expect(rendered).to include('4 tags') + end + end end end diff --git a/yarn.lock b/yarn.lock index 45375114d43..fbaddce43c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -995,10 +995,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.78.0.tgz#469493bd6cdd254eb5d1271edeab22bbbee2f4c4" integrity sha512-dBgEB/Q4FRD0NapmNrD86DF1FsV0uSgTx0UOJloHnGE2DNR2P1HQrCmLW2fX+QgN4P9CDAzdi2buVHuholofWw== -"@gitlab/ui@5.32.0": - version "5.32.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.32.0.tgz#21bb70b6c8b68bdcbb53ffebde80ff3cd93851c8" - integrity sha512-xTFz4/WbR1e6zj2xI2DULcAGicA6qidb9Reoa02V5snqWcQY+iHDup/XzgXmttTPCiBlqPIFo/CMhH4gSJWuPQ== +"@gitlab/ui@5.35.0": + version "5.35.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.35.0.tgz#843e9febf1d4ef9b846dc3280e32e3e626c6f9b1" + integrity sha512-PD9hqVlRhwYRPbL+u/gcHew8NfPXbphZ0CQqfIXaWUYdEOMksUtP6DnLToG6S321WjrCMD+IBHnVQxf2juZBxg== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -1008,6 +1008,7 @@ highlight.js "^9.13.1" js-beautify "^1.8.8" lodash "^4.17.14" + resize-observer-polyfill "^1.5.1" url-search-params-polyfill "^5.0.0" vue "^2.6.10" vue-loader "^15.4.2" @@ -10531,6 +10532,11 @@ requizzle@~0.2.1: dependencies: underscore "~1.6.0" +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" |