diff options
107 files changed, 2088 insertions, 1536 deletions
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 5e0fd5109bb..5bfe42618c2 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -82,14 +82,5 @@ export const expandAllFiles = ({ commit }) => { commit(types.EXPAND_ALL_FILES); }; -export default { - setBaseConfig, - fetchDiffFiles, - setInlineDiffViewType, - setParallelDiffViewType, - showCommentForm, - cancelCommentForm, - loadMoreLines, - loadCollapsedDiff, - expandAllFiles, -}; +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/diffs/store/index.js b/app/assets/javascripts/diffs/store/index.js deleted file mode 100644 index e6aa8f5b12a..00000000000 --- a/app/assets/javascripts/diffs/store/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import diffsModule from './modules'; - -Vue.use(Vuex); - -export default new Vuex.Store({ - modules: { - diffs: diffsModule, - }, -}); diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 90505f83b60..20d1ebbe049 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -1,4 +1,4 @@ -import actions from '../actions'; +import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; import createState from './diff_state'; diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e3652fe739e..63d83e307ee 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,50 +1,50 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; +import eventHub from '../event_hub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + loadingIcon, + Icon, + }, + props: { + actions: { + type: Array, + required: false, + default: () => [], }, - components: { - loadingIcon, - Icon, + }, + data() { + return { + isLoading: false, + }; + }, + computed: { + title() { + return 'Deploy to...'; }, - props: { - actions: { - type: Array, - required: false, - default: () => [], - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - title() { - return 'Deploy to...'; - }, - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + }, + methods: { + onClickAction(endpoint) { + this.isLoading = true; - eventHub.$emit('postAction', endpoint); - }, + eventHub.$emit('postAction', { endpoint }); + }, - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } - return !action.playable; - }, + return !action.playable; }, - }; + }, +}; </script> <template> <div @@ -61,10 +61,7 @@ data-toggle="dropdown" > <span> - <icon - :size="12" - name="play" - /> + <icon name="play" /> <i class="fa fa-caret-down" aria-hidden="true" @@ -85,10 +82,6 @@ class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" > - <icon - :size="12" - name="play" - /> <span> {{ action.name }} </span> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index 68195225d50..7446196de13 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,30 +1,30 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import { s__ } from '../../locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { s__ } from '../../locale'; - /** - * Renders the external url link in environments table. - */ - export default { - components: { - Icon, +/** + * Renders the external url link in environments table. + */ +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + externalUrl: { + type: String, + required: true, }, - directives: { - tooltip, + }, + computed: { + title() { + return s__('Environments|Open live environment'); }, - props: { - externalUrl: { - type: String, - required: true, - }, - }, - computed: { - title() { - return s__('Environments|Open'); - }, - }, - }; + }, +}; </script> <template> <a @@ -37,9 +37,6 @@ target="_blank" rel="noopener noreferrer nofollow" > - <icon - :size="12" - name="external-link" - /> + <icon name="external-link" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 5ecdccf63ad..39f3790a286 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,429 +1,450 @@ <script> - import Timeago from 'timeago.js'; - import _ from 'underscore'; - import tooltip from '~/vue_shared/directives/tooltip'; - import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; - import { humanize } from '~/lib/utils/text_utility'; - import ActionsComponent from './environment_actions.vue'; - import ExternalUrlComponent from './environment_external_url.vue'; - import StopComponent from './environment_stop.vue'; - import RollbackComponent from './environment_rollback.vue'; - import TerminalButtonComponent from './environment_terminal_button.vue'; - import MonitoringButtonComponent from './environment_monitoring.vue'; - import CommitComponent from '../../vue_shared/components/commit.vue'; - import eventHub from '../event_hub'; - - /** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ - const timeagoInstance = new Timeago(); - - export default { - components: { - UserAvatarLink, - CommitComponent, - ActionsComponent, - ExternalUrlComponent, - StopComponent, - RollbackComponent, - TerminalButtonComponent, - MonitoringButtonComponent, +import Timeago from 'timeago.js'; +import _ from 'underscore'; +import tooltip from '~/vue_shared/directives/tooltip'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { humanize } from '~/lib/utils/text_utility'; +import ActionsComponent from './environment_actions.vue'; +import ExternalUrlComponent from './environment_external_url.vue'; +import StopComponent from './environment_stop.vue'; +import RollbackComponent from './environment_rollback.vue'; +import TerminalButtonComponent from './environment_terminal_button.vue'; +import MonitoringButtonComponent from './environment_monitoring.vue'; +import CommitComponent from '../../vue_shared/components/commit.vue'; +import eventHub from '../event_hub'; + +/** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ +const timeagoInstance = new Timeago(); + +export default { + components: { + UserAvatarLink, + CommitComponent, + ActionsComponent, + ExternalUrlComponent, + StopComponent, + RollbackComponent, + TerminalButtonComponent, + MonitoringButtonComponent, + }, + + directives: { + tooltip, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), }, - directives: { - tooltip, + canCreateDeployment: { + type: Boolean, + required: false, + default: false, }, - props: { - model: { - type: Object, - required: true, - default: () => ({}), - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + }, + + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return ( + this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0 + ); + }, + + /** + * Returns whether the environment can be stopped. + * + * @returns {Boolean} + */ + canStopEnvironment() { + return this.model && this.model.can_stop; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return ( + this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable + ); + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined + ); + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at + ) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map(action => { + const parsedAction = { + name: humanize(action.name), + play_path: action.play_path, + playable: action.playable, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username + ) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && this.model.last_deployment && this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && this.model.last_deployment && this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path + ) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id + ) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title + ) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author + ) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path + ) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) { + const { deployable } = this.model.last_deployment; + return `${deployable.name} #${deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && this.model.last_deployment && this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; }, - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !_.isEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - const { deployable } = this.model.last_deployment; - return `${deployable.name} #${deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - monitoringUrl() { - if (this.model && this.model.metrics_path) { - return this.model.metrics_path; - } - - return ''; - }, - - displayEnvironmentActions() { - return this.hasManualActions || - this.externalURL || - this.monitoringUrl || - this.hasStopAction || - this.canRetry; - }, + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return ( + this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user) + ); }, - methods: { - onClickFolder() { - eventHub.$emit('toggleFolder', this.model); - }, + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if ( + this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user) + ) { + return this.model.last_deployment.user; + } + return {}; }, - }; + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return ( + !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.deployable) + ); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path + ) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return ( + !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined + ); + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + + displayEnvironmentActions() { + return ( + this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.canStopEnvironment || + this.canRetry + ); + }, + }, + + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model); + }, + }, +}; </script> <template> <div @@ -580,11 +601,6 @@ class="btn-group table-action-buttons" role="group"> - <actions-component - v-if="hasManualActions && canCreateDeployment" - :actions="manualActions" - /> - <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL" @@ -595,21 +611,26 @@ :monitoring-url="monitoringUrl" /> + <actions-component + v-if="hasManualActions && canCreateDeployment" + :actions="manualActions" + /> + <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" /> - <stop-component - v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path" - /> - <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" /> + + <stop-component + v-if="canStopEnvironment" + :environment="model" + /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 947e8c901e9..ccc8419ca6d 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,29 +1,29 @@ <script> - /** - * Renders the Monitoring (Metrics) link in environments table. - */ - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +/** + * Renders the Monitoring (Metrics) link in environments table. + */ +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - Icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + monitoringUrl: { + type: String, + required: true, }, - directives: { - tooltip, + }, + computed: { + title() { + return 'Monitoring'; }, - props: { - monitoringUrl: { - type: String, - required: true, - }, - }, - computed: { - title() { - return 'Monitoring'; - }, - }, - }; + }, +}; </script> <template> <a @@ -35,9 +35,6 @@ data-container="body" rel="noopener noreferrer nofollow" > - <icon - :size="12" - name="chart" - /> + <icon name="chart" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 310835c5ea9..4deeef4beb9 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,56 +1,74 @@ <script> - /** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment`. - * - * Makes a post request when the button is clicked. - */ - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - - export default { - components: { - loadingIcon, +/** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ +import { s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import eventHub from '../event_hub'; +import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; + +export default { + components: { + Icon, + LoadingIcon, + }, + + directives: { + tooltip, + }, + + props: { + retryUrl: { + type: String, + default: '', }, - props: { - retryUrl: { - type: String, - default: '', - }, - - isLastDeployment: { - type: Boolean, - default: true, - }, + + isLastDeployment: { + type: Boolean, + default: true, }, - data() { - return { - isLoading: false, - }; + }, + data() { + return { + isLoading: false, + }; + }, + + computed: { + title() { + return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment'); }, - methods: { - onClick() { - this.isLoading = true; + }, + + methods: { + onClick() { + this.isLoading = true; - eventHub.$emit('postAction', this.retryUrl); - }, + eventHub.$emit('postAction', { endpoint: this.retryUrl }); }, - }; + }, +}; </script> <template> <button + v-tooltip :disabled="isLoading" + :title="title" type="button" class="btn d-none d-sm-none d-md-block" @click="onClick" > - <span v-if="isLastDeployment"> - {{ s__("Environments|Re-deploy") }} - </span> - <span v-else> - {{ s__("Environments|Rollback") }} - </span> + <icon + v-if="isLastDeployment" + name="repeat" /> + <icon + v-else + name="redo"/> <loading-icon v-if="isLoading" /> </button> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index eba58bedd6d..a814b3405f5 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,72 +1,78 @@ <script> - /** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ +/** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ - import $ from 'jquery'; - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import { s__ } from '~/locale'; +import eventHub from '../event_hub'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - loadingIcon, - }, +export default { + components: { + Icon, + LoadingButton, + }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - props: { - stopUrl: { - type: String, - default: '', - }, + props: { + environment: { + type: Object, + required: true, }, + }, - data() { - return { - isLoading: false, - }; - }, + data() { + return { + isLoading: false, + }; + }, - computed: { - title() { - return 'Stop'; - }, + computed: { + title() { + return s__('Environments|Stop environment'); }, + }, - methods: { - onClick() { - // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to stop this environment?')) { - this.isLoading = true; + mounted() { + eventHub.$on('stopEnvironment', this.onStopEnvironment); + }, - $(this.$el).tooltip('dispose'); + beforeDestroy() { + eventHub.$off('stopEnvironment', this.onStopEnvironment); + }, - eventHub.$emit('postAction', this.stopUrl); - } - }, + methods: { + onClick() { + $(this.$el).tooltip('dispose'); + eventHub.$emit('requestStopEnvironment', this.environment); + }, + onStopEnvironment(environment) { + if (this.environment.id === environment.id) { + this.isLoading = true; + } }, - }; + }, +}; </script> <template> - <button + <loading-button v-tooltip - :disabled="isLoading" + :loading="isLoading" :title="title" :aria-label="title" - type="button" - class="btn stop-env-link d-none d-sm-none d-md-block" + container-class="btn btn-danger d-none d-sm-none d-md-block" data-container="body" + data-toggle="modal" + data-target="#stop-environment-modal" @click="onClick" > - <i - class="fa fa-stop stop-env-icon" - aria-hidden="true" - > - </i> - <loading-icon v-if="isLoading" /> - </button> + <icon name="stop"/> + </loading-button> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index f8e3165f8cd..350417e5ad0 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,31 +1,31 @@ <script> - /** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +/** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - Icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + terminalPath: { + type: String, + required: false, + default: '', }, - directives: { - tooltip, + }, + computed: { + title() { + return 'Terminal'; }, - props: { - terminalPath: { - type: String, - required: false, - default: '', - }, - }, - computed: { - title() { - return 'Terminal'; - }, - }, - }; + }, +}; </script> <template> <a @@ -36,9 +36,6 @@ class="btn terminal-button d-none d-sm-none d-md-block" data-container="body" > - <icon - :size="12" - name="terminal" - /> + <icon name="terminal" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index b18f02343d6..8efdfb8abe0 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -5,10 +5,12 @@ import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; + import StopEnvironmentModal from './stop_environment_modal.vue'; export default { components: { emptyState, + StopEnvironmentModal, }, mixins: [ @@ -90,6 +92,8 @@ </script> <template> <div :class="cssContainerClass"> + <stop-environment-modal :environment="environmentInStopModal" /> + <div class="top-area"> <tabs :tabs="tabs" diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue new file mode 100644 index 00000000000..657cc8cd1aa --- /dev/null +++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue @@ -0,0 +1,92 @@ +<script> +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '../event_hub'; + +export default { + id: 'stop-environment-modal', + name: 'StopEnvironmentModal', + + components: { + GlModal, + LoadingButton, + }, + + directives: { + tooltip, + }, + + props: { + environment: { + type: Object, + required: true, + }, + }, + + computed: { + noStopActionMessage() { + return sprintf( + s__( + `Environments|Note that this action will stop the environment, + but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment + due to no “stop environment action” being defined + in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`, + ), + { + emphasisStart: '<strong>', + emphasisEnd: '</strong>', + ciConfigLinkStart: + '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">', + ciConfigLinkEnd: '</a>', + }, + false, + ); + }, + }, + + methods: { + onSubmit() { + eventHub.$emit('stopEnvironment', this.environment); + }, + }, +}; +</script> + +<template> + <gl-modal + :id="$options.id" + :footer-primary-button-text="s__('Environments|Stop environment')" + footer-primary-button-variant="danger" + @submit="onSubmit" + > + <template slot="header"> + <h4 + class="modal-title d-flex mw-100" + > + Stopping + <span + v-tooltip + :title="environment.name" + class="text-truncate ml-1 mr-1 flex-fill" + >{{ environment.name }}</span> + ? + </h4> + </template> + + <p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p> + + <div + v-if="!environment.has_stop_action" + class="warning_message" + > + <p v-html="noStopActionMessage"></p> + <a + href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment" + target="_blank" + rel="noopener noreferrer" + >{{ s__('Environments|Learn more about stopping environments') }}</a> + </div> + </gl-modal> +</template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 5f72a39c5cb..e69bfa0b2cc 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,12 +1,18 @@ <script> import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; + import StopEnvironmentModal from '../components/stop_environment_modal.vue'; export default { + components: { + StopEnvironmentModal, + }, + mixins: [ environmentsMixin, CIPaginationMixin, ], + props: { endpoint: { type: String, @@ -38,6 +44,8 @@ </script> <template> <div :class="cssContainerClass"> + <stop-environment-modal :environment="environmentInStopModal" /> + <div v-if="!isLoading" class="top-area" diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index a7a79dbca70..d88624f7f8d 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -40,6 +40,7 @@ export default { scope: getParameterByName('scope') || 'available', page: getParameterByName('page') || '1', requestData: {}, + environmentInStopModal: {}, }; }, @@ -85,7 +86,7 @@ export default { Flash(s__('Environments|An error occurred while fetching the environments.')); }, - postAction(endpoint) { + postAction({ endpoint, errorMessage }) { if (!this.isMakingRequest) { this.isLoading = true; @@ -93,7 +94,7 @@ export default { .then(() => this.fetchEnvironments()) .catch(() => { this.isLoading = false; - Flash(s__('Environments|An error occurred while making the request.')); + Flash(errorMessage || s__('Environments|An error occurred while making the request.')); }); } }, @@ -106,6 +107,15 @@ export default { .catch(this.errorCallback); }, + updateStopModal(environment) { + this.environmentInStopModal = environment; + }, + + stopEnvironment(environment) { + const endpoint = environment.stop_path; + const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again'); + this.postAction({ endpoint, errorMessage }); + }, }, computed: { @@ -162,9 +172,13 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('requestStopEnvironment', this.updateStopModal); + eventHub.$on('stopEnvironment', this.stopEnvironment); }, - beforeDestroyed() { - eventHub.$off('postAction'); + beforeDestroy() { + eventHub.$off('postAction', this.postAction); + eventHub.$off('requestStopEnvironment', this.updateStopModal); + eventHub.$off('stopEnvironment', this.stopEnvironment); }, }; diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 3b121551aca..4e07ccba91a 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -13,7 +13,7 @@ export default class EnvironmentsService { // eslint-disable-next-line class-methods-use-this postAction(endpoint) { - return axios.post(endpoint, {}, { emulateJSON: true }); + return axios.post(endpoint, {}); } getFolderContent(folderUrl) { diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 963e3a37b39..26482a02e00 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -200,7 +200,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" @click="cancelHandler()"> - {{ __('Discard draft') }} + Cancel </button> </div> </form> diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 199039f38f7..3144dcc4dc0 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -23,7 +23,7 @@ } .btn-group { - > a { + > .btn:not(.btn-danger) { color: $gl-text-color-secondary; } diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 395c5336ad5..68353e6a210 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_create_deployment!, only: [:stop] + before_action :authorize_stop_environment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] @@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController def environment @environment ||= project.environments.find(params[:id]) end + + def authorize_stop_environment! + access_denied! unless can?(current_user, :stop_environment, environment) + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 1ad2e93c85f..dc6551fc761 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo deployment = environment.first_deployment_for(@merge_request.diff_head_sha) stop_url = - if environment.stop_action? && can?(current_user, :create_deployment, environment) + if can?(current_user, :stop_environment, environment) stop_project_environment_path(project, environment) end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ef1bf283d0c..358b896702b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -251,6 +251,7 @@ module ApplicationSettingsHelper :user_oauth_applications, :version_check_enabled, :allow_local_requests_from_hooks_and_services, + :hide_third_party_offers, :enforce_terms, :terms, :mirror_available diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index c24d340d184..8fd0b6f14c6 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -4,6 +4,7 @@ module ClustersHelper end def render_gcp_signup_offer + return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers? return unless show_gcp_signup_offer? content_tag :section, class: 'no-animate expanded' do diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index 6edaf78de1b..4b9f6bd2caf 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -3,7 +3,7 @@ module PipelineSchedulesHelper ActiveSupport::TimeZone.all.map do |timezone| { name: timezone.name, - offset: timezone.utc_offset, + offset: timezone.now.utc_offset, identifier: timezone.tzinfo.identifier } end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 271e839692a..336385f6798 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -5,9 +5,13 @@ module TimeHelper seconds = interval_in_seconds - minutes * 60 if minutes >= 1 - "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + if seconds % 60 == 0 + pluralize(minutes, "minute") + else + [pluralize(minutes, "minute"), pluralize(seconds, "second")].to_sentence + end else - "#{pluralize(seconds, "second")}" + pluralize(seconds, "second") end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bddeb8b0352..f770b219422 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -294,6 +294,7 @@ class ApplicationSetting < ActiveRecord::Base gitaly_timeout_medium: 30, gitaly_timeout_default: 55, allow_local_requests_from_hooks_and_services: false, + hide_third_party_offers: false, mirror_available: true } end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 44103e3bc4f..d8ddb4bc667 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -437,9 +437,9 @@ module Ci end def artifacts_metadata_entry(path, **options) - artifacts_metadata.use_file do |metadata_path| + artifacts_metadata.open do |metadata_stream| metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( - metadata_path, + metadata_stream, path, **options) diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index 375a5535359..978dc3a7c81 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -1,9 +1,13 @@ class EnvironmentPolicy < BasePolicy delegate { @subject.project } - condition(:stop_action_allowed) do - @subject.stop_action? && can?(:update_build, @subject.stop_action) + condition(:stop_with_deployment_allowed) do + @subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action) end - rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment + condition(:stop_with_update_allowed) do + !@subject.stop_action? && can?(:update_environment, @subject) + end + + rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index ba0ae6ba8a0..0fc3f92b151 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity expose :external_url expose :environment_type expose :last_deployment, using: DeploymentEntity - expose :stop_action? + expose :stop_action?, as: :has_stop_action expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| metrics_project_environment_path(environment.project, environment) @@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity end expose :created_at, :updated_at + + expose :can_stop do |environment| + environment.available? && can?(current_user, :stop_environment, environment) + end + + private + + def current_user + request.current_user + end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 7919f126075..719bd6ef418 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -71,6 +71,28 @@ class GitlabUploader < CarrierWave::Uploader::Base File.join('/', self.class.base_dir, dynamic_segment, filename) end + def cached_size + size + end + + def open + stream = + if file_storage? + File.open(path, "rb") if path + else + ::Gitlab::HttpIO.new(url, cached_size) if url + end + + return unless stream + return stream unless block_given? + + begin + yield(stream) + ensure + stream.close + end + end + private # Designed to be overridden by child uploaders that have a dynamic path diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index 855cf2fc21c..f6af023e0f9 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -18,14 +18,6 @@ class JobArtifactUploader < GitlabUploader dynamic_segment end - def open - if file_storage? - File.open(path, "rb") if path - else - ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url - end - end - private def dynamic_segment diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml new file mode 100644 index 00000000000..c5d775d4bf5 --- /dev/null +++ b/app/views/admin/application_settings/_third_party_offers.html.haml @@ -0,0 +1,13 @@ +- application_setting = local_assigns.fetch(:application_setting) + += form_for application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| + = form_errors(application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :hide_third_party_offers, class: 'form-check-input' + = f.label :hide_third_party_offers, class: 'form-check-label' do + Do not display offers from third parties within GitLab + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index bd43504dd37..5cb8001a364 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,5 +325,16 @@ .settings-content = render partial: 'repository_mirrors_form' +%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Third party offers') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Control the display of third party offers.') + .settings-content + = render 'third_party_offers', application_setting: @application_setting + = render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 37fb8fbab26..3ae9ce6c11f 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -5,7 +5,7 @@ %ol %li = _("Install a Runner compatible with GitLab CI") - = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe + = (_("(check out the %{link} for information on how to install it).") % { link: link }).html_safe %li = _("Specify the following URL during the Runner setup:") %code#coordinator_address= root_url(only_path: false) diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index e0ecf56525a..f4c91377ecb 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -3,13 +3,12 @@ - if actions.present? .btn-group .dropdown - %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } - = custom_icon('icon_play') + %button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') } + = sprite_icon('play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-right - actions.each do |action| - next unless can?(current_user, :update_build, action) %li - = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = custom_icon('icon_play') + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do %span= action.name.humanize diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml index 95f950948ab..281e042c915 100644 --- a/app/views/projects/deployments/_rollback.haml +++ b/app/views/projects/deployments/_rollback.haml @@ -1,6 +1,7 @@ - if can?(current_user, :create_deployment, deployment) && deployment.deployable - = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do + - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do - if deployment.last? - = _("Re-deploy") + = sprite_icon('repeat') - else - = _("Rollback") + = sprite_icon('redo') diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index a264252e095..4694bc39d54 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,4 +1,4 @@ - if environment.external_url && can?(current_user, :read_environment, environment) - = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do + = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do = sprite_icon('external-link') View deployment diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml deleted file mode 100644 index c35f9af2873..00000000000 --- a/app/views/projects/environments/_stop.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- if can?(current_user, :create_deployment, environment) && environment.stop_action? - .inline - = link_to stop_project_environment_path(@project, environment), method: :post, - class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do - = icon('stop', class: 'stop-env-icon') diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index add394a6356..a33bc9d4ce6 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -4,6 +4,33 @@ - page_title "Environments" %div{ class: container_class } + - if can?(current_user, :stop_environment, @environment) + #stop-environment-modal.modal.fade{ tabindex: -1 } + .modal-dialog + .modal-content + .modal-header + %h4.modal-title.d-flex.mw-100 + Stopping + %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } } + = @environment.name + ? + .modal-body + %p= s_('Environments|Are you sure you want to stop this environment?') + - unless @environment.stop_action? + .warning_message + %p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe, + emphasis_end: '</strong>'.html_safe, + ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe, + ci_config_link_end: '</a>'.html_safe } + %a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment', + target: '_blank', + rel: 'noopener noreferrer' } + = s_('Environments|Learn more about stopping environments') + .modal-footer + = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' } + = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do + = s_('Environments|Stop environment') + .row.top-area.adjust .col-md-7 %h3.page-title= @environment.name @@ -15,7 +42,10 @@ - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn' - if can?(current_user, :stop_environment, @environment) - = link_to 'Stop', stop_project_environment_path(@project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post + = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal', + target: '#stop-environment-modal' } do + = sprite_icon('stop') + = s_('Environments|Stop') .environments-container - if @deployments.blank? diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 6b2715b47a7..c360f1ffe2a 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -40,5 +40,5 @@ = yield(:note_actions) - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } } + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } Discard draft diff --git a/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml b/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml new file mode 100644 index 00000000000..cabe5216045 --- /dev/null +++ b/changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Adds with_projects optional parameter to GET /groups/:id API endpoint +merge_request: 20494 +author: +type: changed diff --git a/changelogs/unreleased/48537-update-avatar-only-via-api.yml b/changelogs/unreleased/48537-update-avatar-only-via-api.yml new file mode 100644 index 00000000000..9b3ab946cc1 --- /dev/null +++ b/changelogs/unreleased/48537-update-avatar-only-via-api.yml @@ -0,0 +1,5 @@ +--- +title: Allow updating a project's avatar without other params +merge_request: +author: Jamie Schembri +type: fixed diff --git a/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml b/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml new file mode 100644 index 00000000000..575767df912 --- /dev/null +++ b/changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml @@ -0,0 +1,5 @@ +--- +title: Add option to hide third party offers in admin application settings +merge_request: 20379 +author: +type: added diff --git a/changelogs/unreleased/48951-clean-up.yml b/changelogs/unreleased/48951-clean-up.yml new file mode 100644 index 00000000000..0102cd43f96 --- /dev/null +++ b/changelogs/unreleased/48951-clean-up.yml @@ -0,0 +1,5 @@ +--- +title: Removes unused vuex code in mr refactor and removes unneeded dependencies +merge_request: 20499 +author: +type: other diff --git a/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml b/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml new file mode 100644 index 00000000000..08376014ad7 --- /dev/null +++ b/changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml @@ -0,0 +1,5 @@ +--- +title: Add support for daylight savings time to pipleline schedules +merge_request: 20145 +author: +type: fixed diff --git a/changelogs/unreleased/improve-metadata-access-performance.yml b/changelogs/unreleased/improve-metadata-access-performance.yml new file mode 100644 index 00000000000..b16fa99dd3b --- /dev/null +++ b/changelogs/unreleased/improve-metadata-access-performance.yml @@ -0,0 +1,5 @@ +--- +title: Access metadata directly from Object Storage +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/rails5-mysql-rename-column.yml b/changelogs/unreleased/rails5-mysql-rename-column.yml new file mode 100644 index 00000000000..cbae9250744 --- /dev/null +++ b/changelogs/unreleased/rails5-mysql-rename-column.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 MySQL fix rename_column as part of cleanup_concurrent_column_type_change +merge_request: 20514 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/winh-stop-all-environments.yml b/changelogs/unreleased/winh-stop-all-environments.yml new file mode 100644 index 00000000000..6e5f2f506d9 --- /dev/null +++ b/changelogs/unreleased/winh-stop-all-environments.yml @@ -0,0 +1,5 @@ +--- +title: Support manually stopping any environment from the UI +merge_request: 20077 +author: +type: changed diff --git a/config/initializers/active_record_table_definition.rb b/config/initializers/active_record_table_definition.rb index 8e3a1c7a62f..a71069f27a3 100644 --- a/config/initializers/active_record_table_definition.rb +++ b/config/initializers/active_record_table_definition.rb @@ -29,6 +29,11 @@ module ActiveRecord def datetime_with_timezone(column_name, **options) column(column_name, :datetime_with_timezone, options) end + + # Disable timestamp alias to datetime + def aliased_types(name, fallback) + fallback + end end end end diff --git a/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb new file mode 100644 index 00000000000..6631c5d1b6c --- /dev/null +++ b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb @@ -0,0 +1,18 @@ +class AddHideThirdPartyOffersToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :hide_third_party_offers, + :boolean, + default: false, + allow_null: false) + end + + def down + remove_column(:application_settings, :hide_third_party_offers) + end +end diff --git a/db/schema.rb b/db/schema.rb index 1898dfc6022..75a1960e2f8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180702120647) do +ActiveRecord::Schema.define(version: 20180704204006) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -167,6 +167,7 @@ ActiveRecord::Schema.define(version: 20180702120647) do t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "enforce_terms", default: false t.boolean "mirror_available", default: true, null: false + t.boolean "hide_third_party_offers", default: false, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/index.md b/doc/administration/index.md index 922cc45d8c4..88190b2df5f 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -45,6 +45,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. - [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code. - [Enforcing Terms of Service](../user/admin_area/settings/terms.md) +- [Third party offers](../user/admin_area/settings/third_party_offers.md) #### Customizing GitLab's appearance diff --git a/doc/api/groups.md b/doc/api/groups.md index 53d72509423..11de75039ee 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -210,6 +210,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | +| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4 @@ -361,6 +362,30 @@ Example response: } ``` +When adding the parameter `with_projects=false`, projects will not be returned. + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4?with_projects=false +``` + +Example response: + +```json +{ + "id": 4, + "name": "Twitter", + "path": "twitter", + "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.", + "visibility": "public", + "avatar_url": null, + "web_url": "https://gitlab.example.com/groups/twitter", + "request_access_enabled": false, + "full_name": "Twitter", + "full_path": "twitter", + "parent_id": null +} +``` + ## New group Creates a new project group. Available only for users who can create groups. diff --git a/doc/api/projects.md b/doc/api/projects.md index 1e06f6d01f3..a35c2a56992 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -34,7 +34,7 @@ There are currently three options for `merge_method` to choose from: ## List all projects Get a list of all visible projects across GitLab for the authenticated user. -When accessed without authentication, only public projects are returned. +When accessed without authentication, only public projects with "simple" fields are returned. ``` GET /projects @@ -47,7 +47,7 @@ GET /projects | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of projects matching the search criteria | -| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `owned` | boolean | no | Limit by projects owned by the current user | | `membership` | boolean | no | Limit by projects that the current user is a member of | | `starred` | boolean | no | Limit by projects starred by the current user | @@ -56,6 +56,41 @@ GET /projects | `with_issues_enabled` | boolean | no | Limit by enabled issues feature | | `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature | +When `simple=true` or the user is unauthenticated this returns something like: + +```json +[ + { + "id": 4, + "description": null, + "default_branch": "master", + "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", + "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", + "web_url": "http://example.com/diaspora/diaspora-client", + "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md", + "tag_list": [ + "example", + "disapora client" + ], + "name": "Diaspora Client", + "name_with_namespace": "Diaspora / Diaspora Client", + "path": "diaspora-client", + "path_with_namespace": "diaspora/diaspora-client", + "created_at": "2013-09-30T13:46:02Z", + "last_activity_at": "2013-09-30T13:46:02Z", + "forks_count": 0, + "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png", + "star_count": 0, + }, + { + "id": 6, + "description": null, + "default_branch": "master", +... +``` + +When the user is authenticated and `simple` is not set this returns something like: + ```json [ { @@ -235,7 +270,7 @@ GET /users/:user_id/projects | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of projects matching the search criteria | -| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `owned` | boolean | no | Limit by projects owned by the current user | | `membership` | boolean | no | Limit by projects that the current user is a member of | | `starred` | boolean | no | Limit by projects starred by the current user | @@ -723,7 +758,7 @@ GET /projects/:id/forks | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Return list of projects matching the search criteria | -| `simple` | boolean | no | Return only the ID, URL, name, and path of each project | +| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. | | `owned` | boolean | no | Limit by projects owned by the current user | | `membership` | boolean | no | Limit by projects that the current user is a member of | | `starred` | boolean | no | Limit by projects starred by the current user | diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index aa31e172641..811f4d1f07a 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -43,9 +43,9 @@ There's also a collection of repositories with [example projects](https://gitlab - [Using `dpl` as deployment tool](deployment/README.md) - [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -## Code quality analysis +## Code Quality analysis -[Analyze code quality with the Code Climate CLI](code_climate.md). +**(Starter)** [Analyze your project's Code Quality](code_quality.md). ## Static Application Security Testing (SAST) diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index 2c8865c6e50..b34637efc8d 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -1,49 +1,6 @@ -# Analyze project code quality with Code Climate CLI +--- +redirect_from: 'https://docs.gitlab.com/ee/ci/examples/code_climate.html' +redirect_to: code_quality.md +--- -This example shows how to run [Code Climate CLI][cli] on your code by using -GitLab CI and Docker. - -First, you need GitLab Runner with [docker-in-docker executor][dind]. - -Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `code_quality`: - -```yaml -code_quality: - image: docker:stable - variables: - DOCKER_DRIVER: overlay2 - allow_failure: true - services: - - docker:stable-dind - script: - - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run - --env SOURCE_CODE="$PWD" - --volume "$PWD":/code - --volume /var/run/docker.sock:/var/run/docker.sock - "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code - artifacts: - paths: [gl-code-quality-report.json] -``` - -The above example will create a `code_quality` job in your CI/CD pipeline which -will scan your source code for code quality issues. The report will be saved -as an artifact that you can later download and analyze. - -TIP: **Tip:** -Starting with [GitLab Starter][ee] 9.3, this information will -be automatically extracted and shown right in the merge request widget. To do -so, the CI/CD job must be named `code_quality` and the artifact path must be -`gl-code-quality-report.json`. -[Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). - -CAUTION: **Caution:** -Code Quality was previously using `codeclimate` and `codequality` for job name and -`codeclimate.json` for the artifact name. While these old names -are still maintained they have been deprecated with GitLab 11.0 and may be removed -in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` -configuration to reflect that change. - -[cli]: https://github.com/codeclimate/codeclimate -[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor -[ee]: https://about.gitlab.com/pricing/ +This document was moved to [another location](code_quality.md). diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md new file mode 100644 index 00000000000..2a7040ecdeb --- /dev/null +++ b/doc/ci/examples/code_quality.md @@ -0,0 +1,49 @@ +# Analyze your project's Code Quality + +This example shows how to run Code Quality on your code by using GitLab CI/CD +and Docker. + +First, you need GitLab Runner with [docker-in-docker executor][dind]. + +Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `code_quality`: + +```yaml +code_quality: + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + script: + - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') + - docker run + --env SOURCE_CODE="$PWD" + --volume "$PWD":/code + --volume /var/run/docker.sock:/var/run/docker.sock + "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code + artifacts: + paths: [gl-code-quality-report.json] +``` + +The above example will create a `code_quality` job in your CI/CD pipeline which +will scan your source code for code quality issues. The report will be saved +as an artifact that you can later download and analyze. + +TIP: **Tip:** +Starting with [GitLab Starter][ee] 9.3, this information will +be automatically extracted and shown right in the merge request widget. To do +so, the CI/CD job must be named `code_quality` and the artifact path must be +`gl-code-quality-report.json`. +[Learn more on Code Quality in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html). + +CAUTION: **Caution:** +Code Quality was previously using `codeclimate` and `codequality` for job name and +`codeclimate.json` for the artifact name. While these old names +are still maintained they have been deprecated with GitLab 11.0 and may be removed +in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` +configuration to reflect that change. + +[cli]: https://github.com/codeclimate/codeclimate +[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor +[ee]: https://about.gitlab.com/pricing/ diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index e31ee087358..219b98ac696 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -425,7 +425,7 @@ There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` th ```javascript import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper.js' +import mountComponent from 'spec/helpers/vue_mount_component_helper' import component from 'component.vue' const Component = Vue.extend(component); diff --git a/doc/install/installation.md b/doc/install/installation.md index 259d8f73a22..4b68090f8d3 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -447,6 +447,15 @@ You can specify a different Git repository by providing it as an extra parameter sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +### Install gitlab-pages + +GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways. + + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) + sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 9094d1f2419..2afcb052536 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -22,8 +22,8 @@ Bitbucket.org. > **Note:** GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with -GitLab. You are encouraged to upgrade your GitLab instance if you haven't done -already. If you're using GitLab 8.14 and below, [use the previous integration +GitLab. You are encouraged to upgrade your GitLab instance if you haven't done so +already. If you're using GitLab 8.14 or below, [use the previous integration docs][bb-old]. To enable the Bitbucket OmniAuth provider you must register your application @@ -64,7 +64,7 @@ you to use. 1. Select **Save**. 1. Select your newly created OAuth consumer and you should now see a Key and - Secret in the list of OAuth customers. Keep this page open as you continue + Secret in the list of OAuth consumers. Keep this page open as you continue the configuration. ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png) @@ -114,8 +114,8 @@ you to use. from the Bitbucket application page. 1. Save the configuration file. -1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you - installed GitLab via Omnibus or from source respectively. +1. For the changes to take effect, [reconfigure GitLab][] if you installed via + Omnibus, or [restart][] if installed from source. On the sign in page there should now be a Bitbucket icon below the regular sign in form. Click the icon to begin the authentication process. Bitbucket will ask @@ -127,12 +127,12 @@ well, the user will be returned to GitLab and will be signed in. Once the above configuration is set up, you can use Bitbucket to sign into GitLab and [start importing your projects][bb-import]. -If you don't want to enable signing in with Bitbucket but just want to import -projects from Bitbucket, you could [disable it in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources). +If you want to import projects from Bitbucket, but don't want to enable signing in, +you can [disable Sign-Ins in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources). [init-oauth]: omniauth.md#initial-omniauth-configuration [bb-import]: ../workflow/importing/import_projects_from_bitbucket.md [bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md [bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints -[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure -[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source +[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index bb323705ab3..d04829eaeb8 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -297,7 +297,7 @@ out. In GitLab Starter, differences between the source and target branches are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html). ### Auto SAST **[ULTIMATE]** diff --git a/doc/update/11.0-to-11.1.md b/doc/update/11.0-to-11.1.md index 306bd417ebf..3f10a7edb8a 100644 --- a/doc/update/11.0-to-11.1.md +++ b/doc/update/11.0-to-11.1.md @@ -187,7 +187,24 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) sudo -u git -H make ``` -### 10. Update MySQL permissions +### 10. Update gitlab-pages + +#### Only needed if you use GitLab Pages. + +Install and compile gitlab-pages. GitLab-Pages uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. Update MySQL permissions If you are using MySQL you need to grant the GitLab user the necessary permissions on the database: @@ -209,7 +226,7 @@ You can make this setting permanent by adding it to your `my.cnf`: log_bin_trust_function_creators=1 ``` -### 11. Update configuration files +### 12. Update configuration files #### New configuration options for `gitlab.yml` @@ -283,7 +300,7 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 12. Install libs, migrations, etc. +### 13. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -313,14 +330,14 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). -### 13. Start application +### 14. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 14. Check application status +### 15. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md new file mode 100644 index 00000000000..177251b52b9 --- /dev/null +++ b/doc/user/admin_area/settings/third_party_offers.md @@ -0,0 +1,6 @@ +# Third party offers + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379) +> in [GitLab Core](https://about.gitlab.com/pricing/) 11.1 + +The display of third party offers can be controlled in the Admin Area -> Settings page. diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 483a54051d7..86ecf33ed31 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -35,7 +35,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]** - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]** -- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]** +- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** ## Use cases @@ -43,7 +43,7 @@ A. Consider you are a software developer working in a team: 1. You checkout a new branch, and submit your changes through a merge request 1. You gather feedback from your team -1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) **[STARTER]** +1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** 1. You build and test your changes with GitLab CI/CD 1. You request the approval from your manager 1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter) diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 5c63ec028d9..fa828f43001 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -89,9 +89,10 @@ module API requires :environment_id, type: Integer, desc: 'The environment ID' end post ':id/environments/:environment_id/stop' do - authorize! :create_deployment, user_project + authorize! :read_environment, user_project environment = user_project.environments.find(params[:environment_id]) + authorize! :stop_environment, environment environment.stop_with_action!(current_user) diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f633dd88d06..797b04df059 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -150,12 +150,13 @@ module API end params do use :with_custom_attributes + optional :with_projects, type: Boolean, default: true, desc: 'Omit project details' end get ":id" do group = find_group!(params[:id]) options = { - with: Entities::GroupDetail, + with: params[:with_projects] ? Entities::GroupDetail : Entities::Group, current_user: current_user } diff --git a/lib/api/projects.rb b/lib/api/projects.rb index b83da00502d..8273abe48c9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -260,7 +260,8 @@ module API :snippets_enabled, :tag_list, :visibility, - :wiki_enabled + :wiki_enabled, + :avatar ] optional :name, type: String, desc: 'The name of the project' optional :default_branch, type: String, desc: 'The default branch of the project' diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index 0bbd60d8ffe..375d8bc1ff5 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -7,14 +7,15 @@ module Gitlab module Artifacts class Metadata ParserError = Class.new(StandardError) + InvalidStreamError = Class.new(StandardError) VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)} - attr_reader :file, :path, :full_version + attr_reader :stream, :path, :full_version - def initialize(file, path, **opts) - @file, @path, @opts = file, path, opts + def initialize(stream, path, **opts) + @stream, @path, @opts = stream, path, opts @full_version = read_version end @@ -103,7 +104,17 @@ module Gitlab end def gzip(&block) - Zlib::GzipReader.open(@file, &block) + raise InvalidStreamError, "Invalid stream" unless @stream + + # restart gzip reading + @stream.seek(0) + + gz = Zlib::GzipReader.new(@stream) + yield(gz) + rescue Zlib::Error => e + raise InvalidStreamError, e.message + ensure + gz&.finish end end end diff --git a/lib/gitlab/ci/trace/http_io.rb b/lib/gitlab/ci/trace/http_io.rb deleted file mode 100644 index 8788af57a67..00000000000 --- a/lib/gitlab/ci/trace/http_io.rb +++ /dev/null @@ -1,197 +0,0 @@ -## -# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html) -# source: https://gitlab.com/snippets/1685610 -module Gitlab - module Ci - class Trace - class HttpIO - BUFFER_SIZE = 128.kilobytes - - InvalidURLError = Class.new(StandardError) - FailedToGetChunkError = Class.new(StandardError) - - attr_reader :uri, :size - attr_reader :tell - attr_reader :chunk, :chunk_range - - alias_method :pos, :tell - - def initialize(url, size) - raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url) - - @uri = URI(url) - @size = size - @tell = 0 - end - - def close - # no-op - end - - def binmode - # no-op - end - - def binmode? - true - end - - def path - nil - end - - def url - @uri.to_s - end - - def seek(pos, where = IO::SEEK_SET) - new_pos = - case where - when IO::SEEK_END - size + pos - when IO::SEEK_SET - pos - when IO::SEEK_CUR - tell + pos - else - -1 - end - - raise 'new position is outside of file' if new_pos < 0 || new_pos > size - - @tell = new_pos - end - - def eof? - tell == size - end - - def each_line - until eof? - line = readline - break if line.nil? - - yield(line) - end - end - - def read(length = nil, outbuf = "") - out = "" - - length ||= size - tell - - until length <= 0 || eof? - data = get_chunk - break if data.empty? - - chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min - chunk_data = data.byteslice(0, chunk_bytes) - - out << chunk_data - @tell += chunk_data.bytesize - length -= chunk_data.bytesize - end - - # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality - if outbuf - outbuf.slice!(0, outbuf.bytesize) - outbuf << out - end - - out - end - - def readline - out = "" - - until eof? - data = get_chunk - new_line = data.index("\n") - - if !new_line.nil? - out << data[0..new_line] - @tell += new_line + 1 - break - else - out << data - @tell += data.bytesize - end - end - - out - end - - def write(data) - raise NotImplementedError - end - - def truncate(offset) - raise NotImplementedError - end - - def flush - raise NotImplementedError - end - - def present? - true - end - - private - - ## - # The below methods are not implemented in IO class - # - def in_range? - @chunk_range&.include?(tell) - end - - def get_chunk - unless in_range? - response = Net::HTTP.start(uri.hostname, uri.port, proxy_from_env: true, use_ssl: uri.scheme == 'https') do |http| - http.request(request) - end - - raise FailedToGetChunkError unless response.code == '200' || response.code == '206' - - @chunk = response.body.force_encoding(Encoding::BINARY) - @chunk_range = response.content_range - - ## - # Note: If provider does not return content_range, then we set it as we requested - # Provider: minio - # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 - # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 - # Provider: AWS - # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 - # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 - # Provider: GCS - # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 - # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200 - @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize)) - end - - @chunk[chunk_offset..BUFFER_SIZE] - end - - def request - Net::HTTP::Get.new(uri).tap do |request| - request.set_range(chunk_start, BUFFER_SIZE) - end - end - - def chunk_offset - tell % BUFFER_SIZE - end - - def chunk_start - (tell / BUFFER_SIZE) * BUFFER_SIZE - end - - def chunk_end - [chunk_start + BUFFER_SIZE, size].min - end - end - end - end -end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 76404366e8e..a1a050647b9 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -627,7 +627,7 @@ module Gitlab def update_branch(branch_name, user:, newrev:, oldrev:) gitaly_migrate(:operation_user_update_branch) do |is_enabled| if is_enabled - gitaly_operations_client.user_update_branch(branch_name, user, newrev, oldrev) + gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) else OperationService.new(user, self).update_branch(branch_name, newrev, oldrev) end @@ -904,12 +904,8 @@ module Gitlab end def fetch_source_branch!(source_repository, source_branch, local_ref) - Gitlab::GitalyClient.migrate(:fetch_source_branch) do |is_enabled| - if is_enabled - gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref) - else - rugged_fetch_source_branch(source_repository, source_branch, local_ref) - end + wrapped_gitaly_errors do + gitaly_repository_client.fetch_source_branch(source_repository, source_branch, local_ref) end end @@ -1064,12 +1060,8 @@ module Gitlab end def bundle_to_disk(save_path) - gitaly_migrate(:bundle_to_disk) do |is_enabled| - if is_enabled - gitaly_repository_client.create_bundle(save_path) - else - run_git!(%W(bundle create #{save_path} --all)) - end + wrapped_gitaly_errors do + gitaly_repository_client.create_bundle(save_path) end true diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index db7c29be94b..35808149b90 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -2,6 +2,8 @@ # class return an instance of `GitlabAccessStatus` module Gitlab class GitAccess + include Gitlab::Utils::StrongMemoize + UnauthorizedError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) @@ -26,7 +28,7 @@ module Gitlab PUSH_COMMANDS = %w{ git-receive-pack }.freeze ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS - attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type + attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil) @actor = actor @@ -40,6 +42,8 @@ module Gitlab end def check(cmd, changes) + @changes = changes + check_protocol! check_valid_actor! check_active_user! @@ -58,7 +62,7 @@ module Gitlab when *DOWNLOAD_COMMANDS check_download_access! when *PUSH_COMMANDS - check_push_access!(changes) + check_push_access! end true @@ -218,7 +222,7 @@ module Gitlab end end - def check_push_access!(changes) + def check_push_access! if project.repository_read_only? raise UnauthorizedError, ERROR_MESSAGES[:read_only] end @@ -235,17 +239,15 @@ module Gitlab return if changes.blank? # Allow access this is needed for EE. - check_change_access!(changes) + check_change_access! end - def check_change_access!(changes) + def check_change_access! # If there are worktrees with a HEAD pointing to a non-existent object, # calls to `git rev-list --all` will fail in git 2.15+. This should also # clear stale lock files. project.repository.clean_stale_repository_files - changes_list = Gitlab::ChangesList.new(changes) - # Iterate over all changes to find if user allowed all of them to be applied changes_list.each.with_index do |change, index| first_change = index == 0 @@ -321,6 +323,10 @@ module Gitlab protected + def changes_list + @changes_list ||= Gitlab::ChangesList.new(changes) + end + def user return @user if defined?(@user) diff --git a/lib/gitlab/gitaly_client/conflicts_service.rb b/lib/gitlab/gitaly_client/conflicts_service.rb index b1a01b185e6..aa7e03301f5 100644 --- a/lib/gitlab/gitaly_client/conflicts_service.rb +++ b/lib/gitlab/gitaly_client/conflicts_service.rb @@ -25,10 +25,12 @@ module Gitlab def conflicts? list_conflict_files.any? - rescue GRPC::FailedPrecondition - # The server raises this exception when it encounters ConflictSideMissing, which - # means a conflict exists but its `theirs` or `ours` data is nil due to a non-existent - # file in one of the trees. + rescue GRPC::FailedPrecondition, GRPC::Unknown + # The server raises FailedPrecondition when it encounters + # ConflictSideMissing, which means a conflict exists but its `theirs` or + # `ours` data is nil due to a non-existent file in one of the trees. + # + # GRPC::Unknown comes from Rugged::ReferenceError and Rugged::OdbError. true end diff --git a/lib/gitlab/http_io.rb b/lib/gitlab/http_io.rb new file mode 100644 index 00000000000..ce24817db54 --- /dev/null +++ b/lib/gitlab/http_io.rb @@ -0,0 +1,193 @@ +## +# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html) +# source: https://gitlab.com/snippets/1685610 +module Gitlab + class HttpIO + BUFFER_SIZE = 128.kilobytes + + InvalidURLError = Class.new(StandardError) + FailedToGetChunkError = Class.new(StandardError) + + attr_reader :uri, :size + attr_reader :tell + attr_reader :chunk, :chunk_range + + alias_method :pos, :tell + + def initialize(url, size) + raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url) + + @uri = URI(url) + @size = size + @tell = 0 + end + + def close + # no-op + end + + def binmode + # no-op + end + + def binmode? + true + end + + def path + nil + end + + def url + @uri.to_s + end + + def seek(pos, where = IO::SEEK_SET) + new_pos = + case where + when IO::SEEK_END + size + pos + when IO::SEEK_SET + pos + when IO::SEEK_CUR + tell + pos + else + -1 + end + + raise 'new position is outside of file' if new_pos < 0 || new_pos > size + + @tell = new_pos + end + + def eof? + tell == size + end + + def each_line + until eof? + line = readline + break if line.nil? + + yield(line) + end + end + + def read(length = nil, outbuf = "") + out = "" + + length ||= size - tell + + until length <= 0 || eof? + data = get_chunk + break if data.empty? + + chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min + chunk_data = data.byteslice(0, chunk_bytes) + + out << chunk_data + @tell += chunk_data.bytesize + length -= chunk_data.bytesize + end + + # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality + if outbuf + outbuf.slice!(0, outbuf.bytesize) + outbuf << out + end + + out + end + + def readline + out = "" + + until eof? + data = get_chunk + new_line = data.index("\n") + + if !new_line.nil? + out << data[0..new_line] + @tell += new_line + 1 + break + else + out << data + @tell += data.bytesize + end + end + + out + end + + def write(data) + raise NotImplementedError + end + + def truncate(offset) + raise NotImplementedError + end + + def flush + raise NotImplementedError + end + + def present? + true + end + + private + + ## + # The below methods are not implemented in IO class + # + def in_range? + @chunk_range&.include?(tell) + end + + def get_chunk + unless in_range? + response = Net::HTTP.start(uri.hostname, uri.port, proxy_from_env: true, use_ssl: uri.scheme == 'https') do |http| + http.request(request) + end + + raise FailedToGetChunkError unless response.code == '200' || response.code == '206' + + @chunk = response.body.force_encoding(Encoding::BINARY) + @chunk_range = response.content_range + + ## + # Note: If provider does not return content_range, then we set it as we requested + # Provider: minio + # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 + # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 + # Provider: AWS + # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 + # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 + # Provider: GCS + # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 + # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200 + @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize)) + end + + @chunk[chunk_offset..BUFFER_SIZE] + end + + def request + Net::HTTP::Get.new(uri).tap do |request| + request.set_range(chunk_start, BUFFER_SIZE) + end + end + + def chunk_offset + tell % BUFFER_SIZE + end + + def chunk_start + (tell / BUFFER_SIZE) * BUFFER_SIZE + end + + def chunk_end + [chunk_start + BUFFER_SIZE, size].min + end + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index e222541992a..a17cd27e82d 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -92,21 +92,13 @@ module Gitlab # Ex. # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874 def import_repository(storage, name, url) if url.start_with?('.', '/') raise Error.new("don't use disk paths with import_repository: #{url.inspect}") end relative_path = "#{name}.git" - cmd = gitaly_migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - GitalyGitlabProjects.new(storage, relative_path) - else - # The timeout ensures the subprocess won't hang forever - gitlab_projects(storage, relative_path) - end - end + cmd = GitalyGitlabProjects.new(storage, relative_path) success = cmd.import_project(url, git_timeout) raise Error, cmd.output unless success @@ -126,12 +118,8 @@ module Gitlab # fetch_remote(my_repo, "upstream") # def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) - gitaly_migrate(:fetch_remote) do |is_enabled| - if is_enabled - repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune) - else - local_fetch_remote(repository.storage, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) - end + wrapped_gitaly_errors do + repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune) end end @@ -389,28 +377,6 @@ module Gitlab ) end - def local_fetch_remote(storage_name, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) - vars = { force: forced, tags: !no_tags, prune: prune } - - if ssh_auth&.ssh_import? - if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present? - vars[:ssh_key] = ssh_auth.ssh_private_key - end - - if ssh_auth.ssh_known_hosts.present? - vars[:known_hosts] = ssh_auth.ssh_known_hosts - end - end - - cmd = gitlab_projects(storage_name, repository_relative_path) - - success = cmd.fetch_remote(remote, git_timeout, vars) - - raise Error, cmd.output unless success - - success - end - def gitlab_shell_fast_execute(cmd) output, status = gitlab_shell_fast_execute_helper(cmd) @@ -440,10 +406,6 @@ module Gitlab Gitlab.config.gitlab_shell.git_timeout end - def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block) - wrapped_gitaly_errors { Gitlab::GitalyClient.migrate(method, status: status, &block) } - end - def wrapped_gitaly_errors yield rescue GRPC::NotFound, GRPC::BadStatus => e diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 6df7fe81437..f431352b61e 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -20,16 +20,22 @@ namespace :gettext do end task :regenerate do + pot_file = 'locale/gitlab.pot' # Remove all translated files, this speeds up finding FileUtils.rm Dir['locale/**/gitlab.*'] # remove the `pot` file to ensure it's completely regenerated - FileUtils.rm_f 'locale/gitlab.pot' + FileUtils.rm_f pot_file Rake::Task['gettext:find'].invoke # leave only the required changes. `git checkout -- locale/*/gitlab.po` + # Remove timestamps from the pot file + pot_content = File.read pot_file + pot_content.gsub!(/^"POT?\-(?:Creation|Revision)\-Date\:.*\n/, '') + File.write pot_file, pot_content + puts <<~MSG All done. Please commit the changes to `locale/gitlab.pot`. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ca488053a1f..83ff735580e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,6 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-09 08:28+0200\n" -"PO-Revision-Date: 2018-07-09 08:28+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -128,7 +126,7 @@ msgstr "" msgid "%{unstaged} unstaged and %{staged} staged changes" msgstr "" -msgid "(checkout the %{link} for information on how to install it)." +msgid "(check out the %{link} for information on how to install it)." msgstr "" msgid "+ %{moreCount} more" @@ -1644,6 +1642,9 @@ msgstr "" msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready." msgstr "" +msgid "Control the display of third party offers." +msgstr "" + msgid "Copy URL to clipboard" msgstr "" @@ -2099,9 +2100,18 @@ msgstr "" msgid "Environments|An error occurred while making the request." msgstr "" +msgid "Environments|An error occurred while stopping the environment, please try again" +msgstr "" + +msgid "Environments|Are you sure you want to stop this environment?" +msgstr "" + msgid "Environments|Commit" msgstr "" +msgid "Environments|Deploy to..." +msgstr "" + msgid "Environments|Deployment" msgstr "" @@ -2114,27 +2124,39 @@ msgstr "" msgid "Environments|Job" msgstr "" +msgid "Environments|Learn more about stopping environments" +msgstr "" + msgid "Environments|New environment" msgstr "" msgid "Environments|No deployments yet" msgstr "" -msgid "Environments|Open" +msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file." +msgstr "" + +msgid "Environments|Open live environment" msgstr "" -msgid "Environments|Re-deploy" +msgid "Environments|Re-deploy to environment" msgstr "" msgid "Environments|Read more about environments" msgstr "" -msgid "Environments|Rollback" +msgid "Environments|Rollback environment" msgstr "" msgid "Environments|Show all" msgstr "" +msgid "Environments|Stop" +msgstr "" + +msgid "Environments|Stop environment" +msgstr "" + msgid "Environments|Updated" msgstr "" @@ -3799,9 +3821,6 @@ msgstr "" msgid "Quick actions can be used in the issues description and comment boxes." msgstr "" -msgid "Re-deploy" -msgstr "" - msgid "Read more" msgstr "" @@ -3936,9 +3955,6 @@ msgstr "" msgid "Reviewing (merge request !%{mergeRequestId})" msgstr "" -msgid "Rollback" -msgstr "" - msgid "Runner token" msgstr "" @@ -4600,6 +4616,9 @@ msgstr "" msgid "They can be managed using the %{link}." msgstr "" +msgid "Third party offers" +msgstr "" + msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area." msgstr "" diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index e6332a10265..6be27126383 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -216,17 +216,19 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end context 'when trace artifact is in ObjectStorage' do + let(:url) { 'http://object-storage/trace' } + let(:file_path) { expand_fixture_path('trace/sample_trace') } let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) } before do allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false } - allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url } - allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size } + allow_any_instance_of(JobArtifactUploader).to receive(:url) { url } + allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) } end context 'when there are no network issues' do before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) get_trace end @@ -241,11 +243,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'when there is a network issue' do before do - stub_remote_trace_500 + stub_remote_url_500(url) end it 'returns a trace' do - expect { get_trace }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError) + expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) end end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 9c6758abe86..a852ca689e7 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -272,6 +272,16 @@ describe 'Admin updates settings' do expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true end + it 'Enable hiding third party offers' do + page.within('.as-third-party-offers') do + check 'Do not display offers from third parties within GitLab' + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.hide_third_party_offers).to be true + end + it 'Change Slack Notifications Service template settings' do first(:link, 'Service Templates').click click_link 'Slack notifications' diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb index 53866c32c69..6397df086a7 100644 --- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb +++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb @@ -62,7 +62,7 @@ describe "User adds a comment on a commit", :js do click_diff_line(sample_commit.line_code) expect(page).to have_css(".js-temp-notes-holder form.new-note") - .and have_css(".js-close-discussion-note-form", text: "Discard draft") + .and have_css(".js-close-discussion-note-form", text: "Cancel") # The `Cancel` button closes the current form. The page should not have any open forms after that. find(".js-close-discussion-note-form").click diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 0c34309c1f4..624f7139605 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -166,7 +166,8 @@ describe 'Environment' do end it 'allows to stop environment' do - click_link('Stop') + click_button('Stop') + click_button('Stop environment') # confirm modal expect(page).to have_content('close_app') end @@ -174,7 +175,7 @@ describe 'Environment' do context 'when user has no ability to stop environment' do it 'does not allow to stop environment' do - expect(page).to have_no_link('Stop') + expect(page).not_to have_button('Stop') end end @@ -182,7 +183,7 @@ describe 'Environment' do let(:role) { :reporter } it 'does not show stop button' do - expect(page).not_to have_link('Stop') + expect(page).not_to have_button('Stop') end end end @@ -192,7 +193,7 @@ describe 'Environment' do let(:environment) { create(:environment, project: project, state: :stopped) } it 'does not show stop button' do - expect(page).not_to have_link('Stop') + expect(page).not_to have_button('Stop') end end end @@ -230,7 +231,7 @@ describe 'Environment' do it 'user visits environment page' do visit_environment(environment) - expect(page).to have_link('Stop') + expect(page).to have_button('Stop') end it 'user deletes the branch with running environment' do @@ -242,7 +243,7 @@ describe 'Environment' do visit_environment(environment) - expect(page).to have_no_link('Stop') + expect(page).not_to have_button('Stop') end ## diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 9900c13095e..c2ed753c409 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -10,6 +10,10 @@ describe 'Environments page', :js do sign_in(user) end + def stop_button_selector + %q{button[data-original-title="Stop environment"]} + end + describe 'page tabs' do it 'shows "Available" and "Stopped" tab with links' do visit_environments(project) @@ -120,7 +124,7 @@ describe 'Environments page', :js do end it 'does not show stip button when environment is not stoppable' do - expect(page).not_to have_selector('.stop-env-link') + expect(page).not_to have_selector(stop_button_selector) end end @@ -178,7 +182,7 @@ describe 'Environments page', :js do end it 'shows a stop button' do - expect(page).not_to have_selector('.stop-env-link') + expect(page).not_to have_selector(stop_button_selector) end it 'does not show external link button' do @@ -211,14 +215,14 @@ describe 'Environments page', :js do end it 'shows a stop button' do - expect(page).to have_selector('.stop-env-link') + expect(page).to have_selector(stop_button_selector) end context 'when user is a reporter' do let(:role) { :reporter } it 'does not show stop button' do - expect(page).not_to have_selector('.stop-env-link') + expect(page).not_to have_selector(stop_button_selector) end end end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index 21f35585367..0b371d69ecf 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -4,10 +4,12 @@ describe TimeHelper do describe "#time_interval_in_words" do it "returns minutes and seconds" do intervals_in_words = { - 100 => "1 minute 40 seconds", - 100.32 => "1 minute 40 seconds", - 121 => "2 minutes 1 second", - 3721 => "62 minutes 1 second", + 60 => "1 minute", + 100 => "1 minute and 40 seconds", + 100.32 => "1 minute and 40 seconds", + 120 => "2 minutes", + 121 => "2 minutes and 1 second", + 3721 => "62 minutes and 1 second", 0 => "0 seconds" } diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js index 2d57af6137c..f737e8fa38e 100644 --- a/spec/javascripts/diffs/components/changed_files_spec.js +++ b/spec/javascripts/diffs/components/changed_files_spec.js @@ -1,12 +1,17 @@ import Vue from 'vue'; -import $ from 'jquery'; +import Vuex from 'vuex'; import { mountComponentWithStore } from 'spec/helpers'; -import store from '~/diffs/store'; -import ChangedFiles from '~/diffs/components/changed_files.vue'; +import diffsModule from '~/diffs/store/modules'; +import changedFiles from '~/diffs/components/changed_files.vue'; describe('ChangedFiles', () => { - const Component = Vue.extend(ChangedFiles); - const createComponent = props => mountComponentWithStore(Component, { props, store }); + const Component = Vue.extend(changedFiles); + const store = new Vuex.Store({ + modules: { + diffs: diffsModule, + }, + }); + let vm; beforeEach(() => { @@ -14,6 +19,7 @@ describe('ChangedFiles', () => { <div id="dummy-element"></div> <div class="js-tabs-affix"></div> `); + const props = { diffFiles: [ { @@ -26,7 +32,8 @@ describe('ChangedFiles', () => { }, ], }; - vm = createComponent(props); + + vm = mountComponentWithStore(Component, { props, store }); }); describe('with single file added', () => { @@ -40,58 +47,56 @@ describe('ChangedFiles', () => { }); }); - describe('template', () => { - describe('diff view mode buttons', () => { - let inlineButton; - let parallelButton; + describe('diff view mode buttons', () => { + let inlineButton; + let parallelButton; - beforeEach(() => { - inlineButton = vm.$el.querySelector('.js-inline-diff-button'); - parallelButton = vm.$el.querySelector('.js-parallel-diff-button'); - }); + beforeEach(() => { + inlineButton = vm.$el.querySelector('.js-inline-diff-button'); + parallelButton = vm.$el.querySelector('.js-parallel-diff-button'); + }); + + it('should have Inline and Side-by-side buttons', () => { + expect(inlineButton).toBeDefined(); + expect(parallelButton).toBeDefined(); + }); + + it('should add active class to Inline button', done => { + vm.$store.state.diffs.diffViewType = 'inline'; + + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); - it('should have Inline and Side-by-side buttons', () => { - expect(inlineButton).toBeDefined(); - expect(parallelButton).toBeDefined(); + done(); }); + }); - it('should add active class to Inline button', done => { - vm.$store.state.diffs.diffViewType = 'inline'; + it('should toggle active state of buttons when diff view type changed', done => { + vm.$store.state.diffs.diffViewType = 'parallel'; - vm.$nextTick(() => { - expect(inlineButton.classList.contains('active')).toEqual(true); - expect(parallelButton.classList.contains('active')).toEqual(false); + vm.$nextTick(() => { + expect(inlineButton.classList.contains('active')).toEqual(false); + expect(parallelButton.classList.contains('active')).toEqual(true); - done(); - }); + done(); }); + }); - it('should toggle active state of buttons when diff view type changed', done => { - vm.$store.state.diffs.diffViewType = 'parallel'; + describe('clicking them', () => { + it('should toggle the diff view type', done => { + parallelButton.click(); vm.$nextTick(() => { expect(inlineButton.classList.contains('active')).toEqual(false); expect(parallelButton.classList.contains('active')).toEqual(true); - done(); - }); - }); - - describe('clicking them', () => { - it('should toggle the diff view type', done => { - $(parallelButton).click(); + inlineButton.click(); vm.$nextTick(() => { - expect(inlineButton.classList.contains('active')).toEqual(false); - expect(parallelButton.classList.contains('active')).toEqual(true); - - $(inlineButton).click(); - - vm.$nextTick(() => { - expect(inlineButton.classList.contains('active')).toEqual(true); - expect(parallelButton.classList.contains('active')).toEqual(false); - done(); - }); + expect(inlineButton.classList.contains('active')).toEqual(true); + expect(parallelButton.classList.contains('active')).toEqual(false); + done(); }); }); }); diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js index eb8e49d81fe..79f33c5bc8a 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/javascripts/environments/environment_rollback_spec.js @@ -18,7 +18,7 @@ describe('Rollback Component', () => { }, }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); + expect(component.$el).toHaveSpriteIcon('repeat'); }); it('Should render Rollback label when isLastDeployment is false', () => { @@ -30,6 +30,6 @@ describe('Rollback Component', () => { }, }).$mount(); - expect(component.$el.querySelector('span').textContent).toContain('Rollback'); + expect(component.$el).toHaveSpriteIcon('redo'); }); }); diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js index 3f95faf466a..4d9caa57566 100644 --- a/spec/javascripts/environments/environment_stop_spec.js +++ b/spec/javascripts/environments/environment_stop_spec.js @@ -4,7 +4,6 @@ import stopComp from '~/environments/components/environment_stop.vue'; describe('Stop Component', () => { let StopComponent; let component; - const stopURL = '/stop'; beforeEach(() => { StopComponent = Vue.extend(stopComp); @@ -12,20 +11,13 @@ describe('Stop Component', () => { component = new StopComponent({ propsData: { - stopUrl: stopURL, + environment: {}, }, }).$mount(); }); - describe('computed', () => { - it('title', () => { - expect(component.title).toEqual('Stop'); - }); - }); - it('should render a button to stop the environment', () => { expect(component.$el.tagName).toEqual('BUTTON'); - expect(component.$el.getAttribute('data-original-title')).toEqual('Stop'); - expect(component.$el.getAttribute('aria-label')).toEqual('Stop'); + expect(component.$el.getAttribute('data-original-title')).toEqual('Stop environment'); }); }); diff --git a/spec/javascripts/helpers/wait_for_promises.js b/spec/javascripts/helpers/wait_for_promises.js new file mode 100644 index 00000000000..1d2b53fc770 --- /dev/null +++ b/spec/javascripts/helpers/wait_for_promises.js @@ -0,0 +1 @@ +export default () => new Promise(resolve => requestAnimationFrame(resolve)); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 79e375aa02e..2fcb5566ebc 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -5,6 +5,7 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; import '~/lib/utils/datetime_utility'; import Job from '~/job'; import '~/breakpoints'; +import waitForPromises from 'spec/helpers/wait_for_promises'; describe('Job', () => { const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; @@ -12,10 +13,6 @@ describe('Job', () => { let response; let job; - function waitForPromise() { - return new Promise(resolve => requestAnimationFrame(resolve)); - } - preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { @@ -49,7 +46,7 @@ describe('Job', () => { beforeEach(function (done) { job = new Job(); - waitForPromise() + waitForPromises() .then(done) .catch(done.fail); }); @@ -93,7 +90,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/Update/); expect(job.state).toBe('newstate'); @@ -107,7 +104,7 @@ describe('Job', () => { }; }) .then(() => jasmine.clock().tick(4001)) - .then(waitForPromise) + .then(waitForPromises) .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); expect(job.state).toBe('finalstate'); @@ -126,7 +123,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect($('#build-trace .js-build-output').text()).toMatch(/Update/); @@ -137,7 +134,7 @@ describe('Job', () => { }; }) .then(() => jasmine.clock().tick(4001)) - .then(waitForPromise) + .then(waitForPromises) .then(() => { expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/); @@ -160,7 +157,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); }) @@ -181,7 +178,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect( document.querySelector('.js-truncated-info-size').textContent.trim(), @@ -203,7 +200,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect( document.querySelector('.js-truncated-info-size').textContent.trim(), @@ -219,7 +216,7 @@ describe('Job', () => { }; }) .then(() => jasmine.clock().tick(4001)) - .then(waitForPromise) + .then(waitForPromises) .then(() => { expect( document.querySelector('.js-truncated-info-size').textContent.trim(), @@ -258,7 +255,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(() => { expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); }) @@ -280,7 +277,7 @@ describe('Job', () => { job = new Job(); - waitForPromise() + waitForPromises() .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 60153672214..d9b6dd1d487 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,12 +1,12 @@ import $ from 'jquery'; import _ from 'underscore'; import SmartInterval from '~/smart_interval'; +import waitForPromises from 'spec/helpers/wait_for_promises'; describe('SmartInterval', function () { const DEFAULT_MAX_INTERVAL = 100; const DEFAULT_STARTING_INTERVAL = 5; const DEFAULT_SHORT_TIMEOUT = 75; - const DEFAULT_LONG_TIMEOUT = 1000; const DEFAULT_INCREMENT_FACTOR = 2; function createDefaultSmartInterval(config) { @@ -27,52 +27,65 @@ describe('SmartInterval', function () { return new SmartInterval(defaultParams); } + beforeEach(() => { + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + describe('Increment Interval', function () { - beforeEach(function () { - this.smartInterval = createDefaultSmartInterval(); - }); + it('should increment the interval delay', (done) => { + const smartInterval = createDefaultSmartInterval(); - it('should increment the interval delay', function (done) { - const interval = this.smartInterval; - setTimeout(() => { - const intervalConfig = this.smartInterval.cfg; - const iterationCount = 4; - const maxIntervalAfterIterations = intervalConfig.startingInterval * - (intervalConfig.incrementByFactorOf ** (iterationCount - 1)); // 40 - const currentInterval = interval.getCurrentInterval(); - - // Provide some flexibility for performance of testing environment - expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval); - expect(currentInterval <= maxIntervalAfterIterations).toBeTruthy(); - - done(); - }, DEFAULT_SHORT_TIMEOUT); // 4 iterations, increment by 2x = (5 + 10 + 20 + 40) + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); + + waitForPromises() + .then(() => { + const intervalConfig = smartInterval.cfg; + const iterationCount = 4; + const maxIntervalAfterIterations = intervalConfig.startingInterval * + (intervalConfig.incrementByFactorOf ** iterationCount); + const currentInterval = smartInterval.getCurrentInterval(); + + // Provide some flexibility for performance of testing environment + expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval); + expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations); + }) + .then(done) + .catch(done.fail); }); - it('should not increment past maxInterval', function (done) { - const interval = this.smartInterval; + it('should not increment past maxInterval', (done) => { + const smartInterval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL }); - setTimeout(() => { - const currentInterval = interval.getCurrentInterval(); - expect(currentInterval).toBe(interval.cfg.maxInterval); + jasmine.clock().tick(DEFAULT_STARTING_INTERVAL); + jasmine.clock().tick(DEFAULT_STARTING_INTERVAL * DEFAULT_INCREMENT_FACTOR); - done(); - }, DEFAULT_LONG_TIMEOUT); + waitForPromises() + .then(() => { + const currentInterval = smartInterval.getCurrentInterval(); + expect(currentInterval).toBe(smartInterval.cfg.maxInterval); + }) + .then(done) + .catch(done.fail); }); - it('does not increment while waiting for callback', function () { - jasmine.clock().install(); - + it('does not increment while waiting for callback', done => { const smartInterval = createDefaultSmartInterval({ callback: () => new Promise($.noop), }); jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); - const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR; - expect(smartInterval.getCurrentInterval()).toEqual(oneInterval); - - jasmine.clock().uninstall(); + waitForPromises() + .then(() => { + const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR; + expect(smartInterval.getCurrentInterval()).toEqual(oneInterval); + }) + .then(done) + .catch(done.fail); }); }); @@ -84,34 +97,39 @@ describe('SmartInterval', function () { it('should cancel an interval', function (done) { const interval = this.smartInterval; - setTimeout(() => { - interval.cancel(); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); - const { intervalId } = interval.state; - const currentInterval = interval.getCurrentInterval(); - const intervalLowerLimit = interval.cfg.startingInterval; + interval.cancel(); - expect(intervalId).toBeUndefined(); - expect(currentInterval).toBe(intervalLowerLimit); + waitForPromises() + .then(() => { + const { intervalId } = interval.state; + const currentInterval = interval.getCurrentInterval(); + const intervalLowerLimit = interval.cfg.startingInterval; - done(); - }, DEFAULT_SHORT_TIMEOUT); + expect(intervalId).toBeUndefined(); + expect(currentInterval).toBe(intervalLowerLimit); + }) + .then(done) + .catch(done.fail); }); it('should resume an interval', function (done) { const interval = this.smartInterval; - setTimeout(() => { - interval.cancel(); - - interval.resume(); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); - const { intervalId } = interval.state; + interval.cancel(); - expect(intervalId).toBeTruthy(); + interval.resume(); - done(); - }, DEFAULT_SHORT_TIMEOUT); + waitForPromises() + .then(() => { + const { intervalId } = interval.state; + expect(intervalId).toBeTruthy(); + }) + .then(done) + .catch(done.fail); }); }); @@ -126,64 +144,79 @@ describe('SmartInterval', function () { it('should pause when page is not visible', function (done) { const interval = this.smartInterval; - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); + + waitForPromises() + .then(() => { + expect(interval.state.intervalId).toBeTruthy(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - expect(interval.state.intervalId).toBeUndefined(); - done(); - }, DEFAULT_SHORT_TIMEOUT); + expect(interval.state.intervalId).toBeUndefined(); + }) + .then(done) + .catch(done.fail); }); - it('should change to the hidden interval when page is not visible', function (done) { + it('should change to the hidden interval when page is not visible', done => { const HIDDEN_INTERVAL = 1500; const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL }); - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); - expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && - interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); + + waitForPromises() + .then(() => { + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL && + interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL).toBeTruthy(); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - expect(interval.state.intervalId).toBeTruthy(); - expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); - done(); - }, DEFAULT_SHORT_TIMEOUT); + expect(interval.state.intervalId).toBeTruthy(); + expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL); + }) + .then(done) + .catch(done.fail); }); it('should resume when page is becomes visible at the previous interval', function (done) { const interval = this.smartInterval; - setTimeout(() => { - expect(interval.state.intervalId).toBeTruthy(); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); + waitForPromises() + .then(() => { + expect(interval.state.intervalId).toBeTruthy(); - expect(interval.state.intervalId).toBeUndefined(); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } }); - // simulates triggering of visibilitychange event - interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); + expect(interval.state.intervalId).toBeUndefined(); - expect(interval.state.intervalId).toBeTruthy(); + // simulates triggering of visibilitychange event + interval.handleVisibilityChange({ target: { visibilityState: 'visible' } }); - done(); - }, DEFAULT_SHORT_TIMEOUT); + expect(interval.state.intervalId).toBeTruthy(); + }) + .then(done) + .catch(done.fail); }); it('should cancel on page unload', function (done) { const interval = this.smartInterval; - setTimeout(() => { - $(document).triggerHandler('beforeunload'); - expect(interval.state.intervalId).toBeUndefined(); - expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); - done(); - }, DEFAULT_SHORT_TIMEOUT); + jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT); + + waitForPromises() + .then(() => { + $(document).triggerHandler('beforeunload'); + expect(interval.state.intervalId).toBeUndefined(); + expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); + }) + .then(done) + .catch(done.fail); }); it('should execute callback before first interval', function () { diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index 6a52ae01b2f..e327399d82d 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -2,13 +2,21 @@ require 'spec_helper' describe Gitlab::Ci::Build::Artifacts::Metadata do def metadata(path = '', **opts) - described_class.new(metadata_file_path, path, **opts) + described_class.new(metadata_file_stream, path, **opts) end let(:metadata_file_path) do Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' end + let(:metadata_file_stream) do + File.open(metadata_file_path) if metadata_file_path + end + + after do + metadata_file_stream&.close + end + context 'metadata file exists' do describe '#find_entries! empty string' do subject { metadata('').find_entries! } @@ -86,11 +94,21 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do end context 'metadata file does not exist' do - let(:metadata_file_path) { '' } + let(:metadata_file_path) { nil } + + describe '#find_entries!' do + it 'raises error' do + expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /Invalid stream/) + end + end + end + + context 'metadata file is invalid' do + let(:metadata_file_path) { Rails.root + 'spec/fixtures/ci_build_artifacts.zip' } describe '#find_entries!' do it 'raises error' do - expect { metadata.find_entries! }.to raise_error(Errno::ENOENT) + expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /not in gzip format/) end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e6268a05d44..4f8e6f29255 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1716,59 +1716,51 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#fetch_source_branch!' do - shared_examples '#fetch_source_branch!' do - let(:local_ref) { 'refs/merge-requests/1/head' } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } - let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - after do - ensure_seeds - end + let(:local_ref) { 'refs/merge-requests/1/head' } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:source_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - context 'when the branch exists' do - context 'when the commit does not exist locally' do - let(:source_branch) { 'new-branch-for-fetch-source-branch' } - let(:source_rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } } - let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } + after do + ensure_seeds + end - before do - source_rugged.branches.create(source_branch, new_oid) - end + context 'when the branch exists' do + context 'when the commit does not exist locally' do + let(:source_branch) { 'new-branch-for-fetch-source-branch' } + let(:source_rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } } + let(:new_oid) { new_commit_edit_old_file(source_rugged).oid } - it 'writes the ref' do - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) - expect(repository.commit(local_ref).sha).to eq(new_oid) - end + before do + source_rugged.branches.create(source_branch, new_oid) end - context 'when the commit exists locally' do - let(:source_branch) { 'master' } - let(:expected_oid) { SeedRepo::LastCommit::ID } - - it 'writes the ref' do - # Sanity check: the commit should already exist - expect(repository.commit(expected_oid)).not_to be_nil - - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) - expect(repository.commit(local_ref).sha).to eq(expected_oid) - end + it 'writes the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(new_oid) end end - context 'when the branch does not exist' do - let(:source_branch) { 'definitely-not-master' } + context 'when the commit exists locally' do + let(:source_branch) { 'master' } + let(:expected_oid) { SeedRepo::LastCommit::ID } + + it 'writes the ref' do + # Sanity check: the commit should already exist + expect(repository.commit(expected_oid)).not_to be_nil - it 'does not write the ref' do - expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) - expect(repository.commit(local_ref)).to be_nil + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true) + expect(repository.commit(local_ref).sha).to eq(expected_oid) end end end - it_behaves_like '#fetch_source_branch!' + context 'when the branch does not exist' do + let(:source_branch) { 'definitely-not-master' } - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#fetch_source_branch!' + it 'does not write the ref' do + expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false) + expect(repository.commit(local_ref)).to be_nil + end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ff32025253a..6d11efb42c8 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -13,14 +13,6 @@ describe Gitlab::GitAccess do let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } let(:auth_result_type) { nil } - - let(:access) do - described_class.new(actor, project, - protocol, authentication_abilities: authentication_abilities, - namespace_path: namespace_path, project_path: project_path, - redirected_path: redirected_path, auth_result_type: auth_result_type) - end - let(:changes) { '_any' } let(:push_access_check) { access.check('git-receive-pack', changes) } let(:pull_access_check) { access.check('git-upload-pack', changes) } @@ -724,10 +716,11 @@ describe Gitlab::GitAccess do end describe '#check_push_access!' do + let(:unprotected_branch) { 'unprotected_branch' } + before do merge_into_protected_branch end - let(:unprotected_branch) { 'unprotected_branch' } let(:changes) do { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow", @@ -785,7 +778,7 @@ describe Gitlab::GitAccess do aggregate_failures do matrix.each do |action, allowed| - check = -> { access.send(:check_push_access!, changes[action]) } + check = -> { push_changes(changes[action]) } if allowed expect(&check).not_to raise_error, @@ -1152,6 +1145,17 @@ describe Gitlab::GitAccess do private + def access + described_class.new(actor, project, protocol, + authentication_abilities: authentication_abilities, + namespace_path: namespace_path, project_path: project_path, + redirected_path: redirected_path, auth_result_type: auth_result_type) + end + + def push_changes(changes) + access.check('git-receive-pack', changes) + end + def raise_unauthorized(message) raise_error(Gitlab::GitAccess::UnauthorizedError, message) end diff --git a/spec/lib/gitlab/ci/trace/http_io_spec.rb b/spec/lib/gitlab/http_io_spec.rb index 5474e2f518c..788bddb8f59 100644 --- a/spec/lib/gitlab/ci/trace/http_io_spec.rb +++ b/spec/lib/gitlab/http_io_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe Gitlab::Ci::Trace::HttpIO do +describe Gitlab::HttpIO do include HttpIOHelpers let(:http_io) { described_class.new(url, size) } - let(:url) { remote_trace_url } - let(:size) { remote_trace_size } + + let(:url) { 'http://object-storage/trace' } + let(:file_path) { expand_fixture_path('trace/sample_trace') } + let(:file_body) { File.read(file_path).force_encoding(Encoding::BINARY) } + let(:size) { File.size(file_path) } describe '#close' do subject { http_io.close } @@ -86,10 +89,10 @@ describe Gitlab::Ci::Trace::HttpIO do describe '#each_line' do subject { http_io.each_line } - let(:string_io) { StringIO.new(remote_trace_body) } + let(:string_io) { StringIO.new(file_body) } before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end it 'yields lines' do @@ -99,7 +102,7 @@ describe Gitlab::Ci::Trace::HttpIO do context 'when buckets on GCS' do context 'when BUFFER_SIZE is larger than file size' do before do - stub_remote_trace_200 + stub_remote_url_200(url, file_path) set_larger_buffer_size_than(size) end @@ -117,7 +120,7 @@ describe Gitlab::Ci::Trace::HttpIO do context 'when there are no network issue' do before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end context 'when read whole size' do @@ -129,7 +132,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end @@ -139,7 +142,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end end @@ -153,7 +156,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body[0, length]) + is_expected.to eq(file_body[0, length]) end end @@ -163,7 +166,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body[0, length]) + is_expected.to eq(file_body[0, length]) end end end @@ -177,7 +180,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end @@ -187,7 +190,7 @@ describe Gitlab::Ci::Trace::HttpIO do end it 'reads a trace' do - is_expected.to eq(remote_trace_body) + is_expected.to eq(file_body) end end end @@ -221,11 +224,11 @@ describe Gitlab::Ci::Trace::HttpIO do let(:length) { nil } before do - stub_remote_trace_500 + stub_remote_url_500(url) end it 'reads a trace' do - expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError) + expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) end end end @@ -233,15 +236,15 @@ describe Gitlab::Ci::Trace::HttpIO do describe '#readline' do subject { http_io.readline } - let(:string_io) { StringIO.new(remote_trace_body) } + let(:string_io) { StringIO.new(file_body) } before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) end shared_examples 'all line matching' do it 'reads a line' do - (0...remote_trace_body.lines.count).each do + (0...file_body.lines.count).each do expect(http_io.readline).to eq(string_io.readline) end end @@ -251,11 +254,11 @@ describe Gitlab::Ci::Trace::HttpIO do let(:length) { nil } before do - stub_remote_trace_500 + stub_remote_url_500(url) end it 'reads a trace' do - expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError) + expect { subject }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError) end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index c435f988cdd..f8bf896950e 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -403,46 +403,36 @@ describe Gitlab::Shell do end describe '#create_repository' do - shared_examples '#create_repository' do - let(:repository_storage) { 'default' } - let(:repository_storage_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab.config.repositories.storages[repository_storage].legacy_disk_path - end - end - let(:repo_name) { 'project/path' } - let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - - after do - FileUtils.rm_rf(created_path) + let(:repository_storage) { 'default' } + let(:repository_storage_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[repository_storage].legacy_disk_path end + end + let(:repo_name) { 'project/path' } + let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } - it 'creates a repository' do - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy - - expect(File.stat(created_path).mode & 0o777).to eq(0o770) + after do + FileUtils.rm_rf(created_path) + end - hooks_path = File.join(created_path, 'hooks') - expect(File.lstat(hooks_path)).to be_symlink - expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) - end + it 'creates a repository' do + expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy - it 'returns false when the command fails' do - FileUtils.mkdir_p(File.dirname(created_path)) - # This file will block the creation of the repo's .git directory. That - # should cause #create_repository to fail. - FileUtils.touch(created_path) + expect(File.stat(created_path).mode & 0o777).to eq(0o770) - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy - end + hooks_path = File.join(created_path, 'hooks') + expect(File.lstat(hooks_path)).to be_symlink + expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path) end - context 'with gitaly' do - it_behaves_like '#create_repository' - end + it 'returns false when the command fails' do + FileUtils.mkdir_p(File.dirname(created_path)) + # This file will block the creation of the repo's .git directory. That + # should cause #create_repository to fail. + FileUtils.touch(created_path) - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#create_repository' + expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy end end @@ -513,22 +503,12 @@ describe Gitlab::Shell do end end - shared_examples 'fetch_remote' do |gitaly_on| + describe '#fetch_remote' do def fetch_remote(ssh_auth = nil, prune = true) gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune) end - def expect_gitlab_projects(fail = false, options = {}) - expect(gitlab_projects).to receive(:fetch_remote).with( - 'remote-name', - timeout, - options - ).and_return(!fail) - - allow(gitlab_projects).to receive(:output).and_return('error') if fail - end - - def expect_gitaly_call(fail, options = {}) + def expect_call(fail, options = {}) receive_fetch_remote = if fail receive(:fetch_remote).and_raise(GRPC::NotFound) @@ -539,16 +519,6 @@ describe Gitlab::Shell do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote end - if gitaly_on - def expect_call(fail, options = {}) - expect_gitaly_call(fail, options) - end - else - def expect_call(fail, options = {}) - expect_gitlab_projects(fail, options) - end - end - def build_ssh_auth(opts = {}) defaults = { ssh_import?: true, @@ -634,14 +604,6 @@ describe Gitlab::Shell do expect(fetch_remote(ssh_auth)).to be_truthy end end - end - - describe '#fetch_remote local', :skip_gitaly_mock do - it_should_behave_like 'fetch_remote', false - end - - describe '#fetch_remote gitaly' do - it_should_behave_like 'fetch_remote', true context 'gitaly call' do let(:remote_name) { 'remote-name' } @@ -683,25 +645,6 @@ describe Gitlab::Shell do end.to raise_error(Gitlab::Shell::Error, "error") end end - - context 'without gitaly', :disable_gitaly do - it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true } - - result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - - expect(result).to be_truthy - end - - it 'raises an exception when the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:import_project) { false } - - expect do - gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - end.to raise_error(Gitlab::Shell::Error, "error") - end - end end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7802ff1f5f6..98a1865d347 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -36,22 +36,20 @@ describe Gitlab::Workhorse do allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled) end - context 'when Gitaly workhorse_archive feature is enabled' do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-archive') - expect(params).to include(gitaly_params) - end + expect(key).to eq('Gitlab-Workhorse-Send-Data') + expect(command).to eq('git-archive') + expect(params).to include(gitaly_params) + end - context 'when archive caching is disabled' do - let(:cache_disabled) { true } + context 'when archive caching is disabled' do + let(:cache_disabled) { true } - it 'tells workhorse not to use the cache' do - _, _, params = decode_workhorse_header(subject) - expect(params).to include({ 'DisableCache' => true }) - end + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index a9a45367b4a..effbfb1ba84 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -314,6 +314,17 @@ describe Notify do end end + describe 'that are new with a description' do + subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) } + + it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" + + it 'contains the description' do + is_expected.to have_body_text(merge_request.description) + end + end + describe 'that have been relabeled' do subject { described_class.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 234d2d8aa3a..3c96fe76829 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2687,4 +2687,58 @@ describe Ci::Build do end end end + + describe '#artifacts_metadata_entry' do + set(:build) { create(:ci_build, project: project) } + let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' } + + before do + stub_artifacts_object_storage + end + + subject { build.artifacts_metadata_entry(path) } + + context 'when using local storage' do + let!(:metadata) { create(:ci_job_artifact, :metadata, job: build) } + + context 'for existing file' do + it 'does exist' do + is_expected.to be_exists + end + end + + context 'for non-existing file' do + let(:path) { 'invalid-file' } + + it 'does not exist' do + is_expected.not_to be_exists + end + end + end + + context 'when using remote storage' do + include HttpIOHelpers + + let!(:metadata) { create(:ci_job_artifact, :remote_store, :metadata, job: build) } + let(:file_path) { expand_fixture_path('ci_build_artifacts_metadata.gz') } + + before do + stub_remote_url_206(metadata.file.url, file_path) + end + + context 'for existing file' do + it 'does exist' do + is_expected.to be_exists + end + end + + context 'for non-existing file' do + let(:path) { 'invalid-file' } + + it 'does not exist' do + is_expected.not_to be_exists + end + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index d060ab923d1..caf5d829d21 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -431,6 +431,18 @@ describe Repository do it { is_expected.to be_falsey } end + + context 'non merged branch' do + subject { repository.merged_to_root_ref?('fix') } + + it { is_expected.to be_falsey } + end + + context 'non existent branch' do + subject { repository.merged_to_root_ref?('non_existent_branch') } + + it { is_expected.to be_nil } + end end describe '#can_be_merged?' do @@ -452,17 +464,11 @@ describe Repository do it { is_expected.to be_falsey } end - context 'non merged branch' do - subject { repository.merged_to_root_ref?('fix') } + context 'submodule changes that confuse rugged' do + subject { repository.can_be_merged?('update-gitlab-shell-v-6-0-1', 'update-gitlab-shell-v-6-0-3') } it { is_expected.to be_falsey } end - - context 'non existent branch' do - subject { repository.merged_to_root_ref?('non_existent_branch') } - - it { is_expected.to be_nil } - end end describe '#commit' do diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb index de4cb5b30c5..3728218accc 100644 --- a/spec/policies/environment_policy_spec.rb +++ b/spec/policies/environment_policy_spec.rb @@ -1,57 +1,101 @@ require 'spec_helper' describe EnvironmentPolicy do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } + using RSpec::Parameterized::TableSyntax - let(:environment) do - create(:environment, :with_review_app, project: project) - end + let(:user) { create(:user) } let(:policy) do described_class.new(user, environment) end describe '#rules' do - context 'when user does not have access to the project' do - let(:project) { create(:project, :private, :repository) } + shared_examples 'project permissions' do + context 'with stop action' do + let(:environment) do + create(:environment, :with_review_app, project: project) + end - it 'does not include ability to stop environment' do - expect(policy).to be_disallowed :stop_environment - end - end + where(:access_level, :allowed?) do + nil | false + :guest | false + :reporter | false + :developer | true + :master | true + end - context 'when anonymous user has access to the project' do - let(:project) { create(:project, :public, :repository) } + with_them do + before do + project.add_user(user, access_level) unless access_level.nil? + end - it 'does not include ability to stop environment' do - expect(policy).to be_disallowed :stop_environment - end - end + it { expect(policy.allowed?(:stop_environment)).to be allowed? } + end - context 'when team member has access to the project' do - let(:project) { create(:project, :public, :repository) } + context 'when an admin user' do + let(:user) { create(:user, :admin) } - before do - project.add_developer(user) - end + it { expect(policy).to be_allowed :stop_environment } + end + + context 'with protected branch' do + with_them do + before do + project.add_user(user, access_level) unless access_level.nil? + create(:protected_branch, :no_one_can_push, + name: 'master', project: project) + end - context 'when team member has ability to stop environment' do - it 'does includes ability to stop environment' do - expect(policy).to be_allowed :stop_environment + it { expect(policy).to be_disallowed :stop_environment } + end + + context 'when an admin user' do + let(:user) { create(:user, :admin) } + + it { expect(policy).to be_allowed :stop_environment } + end end end - context 'when team member has no ability to stop environment' do - before do - create(:protected_branch, :no_one_can_push, - name: 'master', project: project) + context 'without stop action' do + let(:environment) do + create(:environment, project: project) + end + + where(:access_level, :allowed?) do + nil | false + :guest | false + :reporter | false + :developer | false + :master | true end - it 'does not include ability to stop environment' do - expect(policy).to be_disallowed :stop_environment + with_them do + before do + project.add_user(user, access_level) unless access_level.nil? + end + + it { expect(policy.allowed?(:stop_environment)).to be allowed? } + end + + context 'when an admin user' do + let(:user) { create(:user, :admin) } + + it { expect(policy).to be_allowed :stop_environment } end end end + + context 'when project is public' do + let(:project) { create(:project, :public, :repository) } + + include_examples 'project permissions' + end + + context 'when project is private' do + let(:project) { create(:project, :private, :repository) } + + include_examples 'project permissions' + end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index da23fdd7dca..5a04e699d60 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -251,14 +251,22 @@ describe API::Groups do projects end + def response_project_ids(json_response, key) + json_response[key].map do |project| + project['id'].to_i + end + end + context 'when unauthenticated' do it 'returns 404 for a private group' do get api("/groups/#{group2.id}") + expect(response).to have_gitlab_http_status(404) end it 'returns 200 for a public group' do get api("/groups/#{group1.id}") + expect(response).to have_gitlab_http_status(200) end @@ -268,7 +276,7 @@ describe API::Groups do get api("/groups/#{public_group.id}") - expect(json_response['projects'].map { |p| p['id'].to_i }) + expect(response_project_ids(json_response, 'projects')) .to contain_exactly(projects[:public].id) end @@ -278,7 +286,7 @@ describe API::Groups do get api("/groups/#{group1.id}") - expect(json_response['shared_projects'].map { |p| p['id'].to_i }) + expect(response_project_ids(json_response, 'shared_projects')) .to contain_exactly(projects[:public].id) end end @@ -309,6 +317,17 @@ describe API::Groups do expect(json_response['shared_projects'][0]['id']).to eq(project.id) end + it "returns one of user1's groups without projects when with_projects option is set to false" do + project = create(:project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + get api("/groups/#{group1.id}", user1), with_projects: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response['projects']).to be_nil + expect(json_response['shared_projects']).to be_nil + end + it "does not return a non existing group" do get api("/groups/1328", user1) @@ -327,7 +346,7 @@ describe API::Groups do get api("/groups/#{public_group.id}", user2) - expect(json_response['projects'].map { |p| p['id'].to_i }) + expect(response_project_ids(json_response, 'projects')) .to contain_exactly(projects[:public].id, projects[:internal].id) end @@ -337,7 +356,7 @@ describe API::Groups do get api("/groups/#{group1.id}", user2) - expect(json_response['shared_projects'].map { |p| p['id'].to_i }) + expect(response_project_ids(json_response, 'shared_projects')) .to contain_exactly(projects[:public].id, projects[:internal].id) end end diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 50d6f4b4d99..0609166ed90 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -535,12 +535,14 @@ describe API::Jobs do context 'authorized user' do context 'when trace is in ObjectStorage' do let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } + let(:url) { 'http://object-storage/trace' } + let(:file_path) { expand_fixture_path('trace/sample_trace') } before do - stub_remote_trace_206 + stub_remote_url_206(url, file_path) allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false } - allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url } - allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size } + allow_any_instance_of(JobArtifactUploader).to receive(:url) { url } + allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) } end it 'returns specific job trace' do diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 98102fcd6a7..e2000ab42e8 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -23,10 +23,10 @@ describe API::Namespaces do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers - expect(group_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') - expect(user_kind_json_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + expect(user_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id') end it "admin: returns an array of all namespaces" do @@ -58,8 +58,8 @@ describe API::Namespaces do owned_group_response = json_response.find { |resource| resource['id'] == group1.id } - expect(owned_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', - 'parent_id', 'members_count_with_descendants') + expect(owned_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', + 'parent_id', 'members_count_with_descendants') end it "returns correct attributes when user cannot admin group" do @@ -69,7 +69,7 @@ describe API::Namespaces do guest_group_response = json_response.find { |resource| resource['id'] == group1.id } - expect(guest_group_response.keys).to contain_exactly('id', 'kind', 'name', 'path', 'full_path', 'parent_id') + expect(guest_group_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', 'parent_id') end it "user: returns an array of namespaces" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index de540ba7a10..15da81b57db 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1526,6 +1526,20 @@ describe API::Projects do expect(response).to have_gitlab_http_status(400) end + + it 'updates avatar' do + project_param = { + avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', + 'image/gif') + } + + put api("/projects/#{project3.id}", user), project_param + + expect(response).to have_gitlab_http_status(200) + expect(json_response['avatar_url']).to eq('http://localhost/uploads/'\ + '-/system/project/avatar/'\ + "#{project3.id}/banana_sample.gif") + end end context 'when authenticated as project master' do diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb index 8f32c5639a1..b7324a26ed2 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe EnvironmentEntity do + let(:request) { double('request') } let(:entity) do - described_class.new(environment, request: double) + described_class.new(environment, request: spy('request')) end let(:environment) { create(:environment) } diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index ca9b520fb38..0f0ab5ac796 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -54,7 +54,9 @@ describe EnvironmentSerializer do context 'when representing environments within folders' do let(:serializer) do - described_class.new(project: project).within_folders + described_class + .new(current_user: user, project: project) + .within_folders end let(:resource) { Environment.all } @@ -123,7 +125,8 @@ describe EnvironmentSerializer do let(:pagination) { { page: 1, per_page: 2 } } let(:serializer) do - described_class.new(project: project) + described_class + .new(current_user: user, project: project) .with_pagination(request, response) end @@ -169,7 +172,8 @@ describe EnvironmentSerializer do context 'when grouping environments within folders' do let(:serializer) do - described_class.new(project: project) + described_class + .new(current_user: user, project: project) .with_pagination(request, response) .within_folders end diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index a5520e7373e..c81fa95e4b7 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe MergeRequests::Conflicts::ListService do describe '#can_be_resolved_in_ui?' do - def create_merge_request(source_branch) - create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', merge_status: :unchecked) do |mr| + def create_merge_request(source_branch, target_branch = 'conflict-start') + create(:merge_request, source_branch: source_branch, target_branch: target_branch, merge_status: :unchecked) do |mr| mr.mark_as_unmergeable end end @@ -84,5 +84,11 @@ describe MergeRequests::Conflicts::ListService do expect(service.can_be_resolved_in_ui?).to be_falsey end + + it 'returns a falsey value when the conflict is in a submodule revision' do + merge_request = create_merge_request('update-gitlab-shell-v-6-0-3', 'update-gitlab-shell-v-6-0-1') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 05a8e6206ae..e531495d917 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -49,7 +49,9 @@ module TestEnv 'add-pdf-file' => 'e774ebd', 'squash-large-files' => '54cec52', 'add-pdf-text-binary' => '79faa7b', - 'add_images_and_changes' => '010d106' + 'add_images_and_changes' => '010d106', + 'update-gitlab-shell-v-6-0-1' => '2f61d70', + 'update-gitlab-shell-v-6-0-3' => 'de78448' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb index 2c68c2cd9a6..42144870eb5 100644 --- a/spec/support/http_io/http_io_helpers.rb +++ b/spec/support/http_io/http_io_helpers.rb @@ -1,65 +1,49 @@ module HttpIOHelpers - def stub_remote_trace_206 - WebMock.stub_request(:get, remote_trace_url) - .to_return { |request| remote_trace_response(request, 206) } + def stub_remote_url_206(url, file_path) + WebMock.stub_request(:get, url) + .to_return { |request| remote_url_response(file_path, request, 206) } end - def stub_remote_trace_200 - WebMock.stub_request(:get, remote_trace_url) - .to_return { |request| remote_trace_response(request, 200) } + def stub_remote_url_200(url, file_path) + WebMock.stub_request(:get, url) + .to_return { |request| remote_url_response(file_path, request, 200) } end - def stub_remote_trace_500 - WebMock.stub_request(:get, remote_trace_url) + def stub_remote_url_500(url) + WebMock.stub_request(:get, url) .to_return(status: [500, "Internal Server Error"]) end - def remote_trace_url - "http://trace.com/trace" - end - - def remote_trace_response(request, responce_status) + def remote_url_response(file_path, request, response_status) range = request.headers['Range'].match(/bytes=(\d+)-(\d+)/) + body = File.read(file_path).force_encoding(Encoding::BINARY) + size = body.bytesize + { - status: responce_status, - headers: remote_trace_response_headers(responce_status, range[1].to_i, range[2].to_i), - body: range_trace_body(range[1].to_i, range[2].to_i) + status: response_status, + headers: remote_url_response_headers(response_status, range[1].to_i, range[2].to_i, size), + body: body[range[1].to_i..range[2].to_i] } end - def remote_trace_response_headers(responce_status, from, to) - headers = { 'Content-Type' => 'text/plain' } - - if responce_status == 206 - headers.merge('Content-Range' => "bytes #{from}-#{to}/#{remote_trace_size}") + def remote_url_response_headers(response_status, from, to, size) + { 'Content-Type' => 'text/plain' }.tap do |headers| + if response_status == 206 + headers.merge('Content-Range' => "bytes #{from}-#{to}/#{size}") + end end - - headers - end - - def range_trace_body(from, to) - remote_trace_body[from..to] - end - - def remote_trace_body - @remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace')) - .force_encoding(Encoding::BINARY) - end - - def remote_trace_size - remote_trace_body.bytesize end def set_smaller_buffer_size_than(file_size) blocks = (file_size / 128) new_size = (blocks / 2) * 128 - stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size) + stub_const("Gitlab::HttpIO::BUFFER_SIZE", new_size) end def set_larger_buffer_size_than(file_size) blocks = (file_size / 128) new_size = (blocks * 2) * 128 - stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size) + stub_const("Gitlab::HttpIO::BUFFER_SIZE", new_size) end end diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index 362f89424d4..44718ed1212 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -68,4 +68,66 @@ describe GitlabUploader do expect(subject.file.path).to match(/#{subject.cache_dir}/) end end + + describe '#open' do + context 'when trace is stored in File storage' do + context 'when file exists' do + let(:file) do + fixture_file_upload('spec/fixtures/trace/sample_trace', 'text/plain') + end + + before do + subject.store!(file) + end + + it 'returns io stream' do + expect(subject.open).to be_a(IO) + end + + it 'when passing block it yields' do + expect { |b| subject.open(&b) }.to yield_control + end + end + + context 'when file does not exist' do + it 'returns nil' do + expect(subject.open).to be_nil + end + + it 'when passing block it does not yield' do + expect { |b| subject.open(&b) }.not_to yield_control + end + end + end + + context 'when trace is stored in Object storage' do + before do + allow(subject).to receive(:file_storage?) { false } + end + + context 'when file exists' do + before do + allow(subject).to receive(:url) { 'http://object_storage.com/trace' } + end + + it 'returns http io stream' do + expect(subject.open).to be_a(Gitlab::HttpIO) + end + + it 'when passing block it yields' do + expect { |b| subject.open(&b) }.to yield_control.once + end + end + + context 'when file does not exist' do + it 'returns nil' do + expect(subject.open).to be_nil + end + + it 'when passing block it does not yield' do + expect { |b| subject.open(&b) }.not_to yield_control + end + end + end + end end diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index 026e4356ed6..3ad5fe7e3b3 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -23,43 +23,6 @@ describe JobArtifactUploader do store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z] end - describe '#open' do - subject { uploader.open } - - context 'when trace is stored in File storage' do - context 'when file exists' do - let(:file) do - fixture_file_upload('spec/fixtures/trace/sample_trace', 'text/plain') - end - - before do - uploader.store!(file) - end - - it 'returns io stream' do - is_expected.to be_a(IO) - end - end - - context 'when file does not exist' do - it 'returns nil' do - is_expected.to be_nil - end - end - end - - context 'when trace is stored in Object storage' do - before do - allow(uploader).to receive(:file_storage?) { false } - allow(uploader).to receive(:url) { 'http://object_storage.com/trace' } - end - - it 'returns http io stream' do - is_expected.to be_a(Gitlab::Ci::Trace::HttpIO) - end - end - end - context 'file is stored in valid local_path' do let(:file) do fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip') |