summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/diffs/store/actions.js13
-rw-r--r--app/assets/javascripts/diffs/store/index.js11
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue87
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue51
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue873
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue49
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue94
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue106
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue53
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue4
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue92
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue8
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js22
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/stylesheets/pages/environments.scss2
-rw-r--r--app/controllers/projects/environments_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/clusters_helper.rb1
-rw-r--r--app/helpers/pipeline_schedules_helper.rb2
-rw-r--r--app/helpers/time_helper.rb8
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/policies/environment_policy.rb10
-rw-r--r--app/serializers/environment_entity.rb12
-rw-r--r--app/uploaders/gitlab_uploader.rb22
-rw-r--r--app/uploaders/job_artifact_uploader.rb8
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml13
-rw-r--r--app/views/admin/application_settings/show.html.haml11
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml7
-rw-r--r--app/views/projects/deployments/_rollback.haml7
-rw-r--r--app/views/projects/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/_stop.html.haml5
-rw-r--r--app/views/projects/environments/show.html.haml32
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--changelogs/unreleased/42415-omit-projects-from-get-group-endpoint.yml5
-rw-r--r--changelogs/unreleased/48537-update-avatar-only-via-api.yml5
-rw-r--r--changelogs/unreleased/48578-disable-gcp-free-credit-banner-at-instance-level.yml5
-rw-r--r--changelogs/unreleased/48951-clean-up.yml5
-rw-r--r--changelogs/unreleased/add-dst-support-to-pipeline-schedule.yml5
-rw-r--r--changelogs/unreleased/improve-metadata-access-performance.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-rename-column.yml5
-rw-r--r--changelogs/unreleased/winh-stop-all-environments.yml5
-rw-r--r--config/initializers/active_record_table_definition.rb5
-rw-r--r--db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb18
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/api/groups.md25
-rw-r--r--doc/api/projects.md43
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/code_climate.md53
-rw-r--r--doc/ci/examples/code_quality.md49
-rw-r--r--doc/development/fe_guide/vue.md2
-rw-r--r--doc/install/installation.md9
-rw-r--r--doc/integration/bitbucket.md18
-rw-r--r--doc/topics/autodevops/index.md2
-rw-r--r--doc/update/11.0-to-11.1.md27
-rw-r--r--doc/user/admin_area/settings/third_party_offers.md6
-rw-r--r--doc/user/project/merge_requests/index.md4
-rw-r--r--lib/api/environments.rb3
-rw-r--r--lib/api/groups.rb3
-rw-r--r--lib/api/projects.rb3
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb19
-rw-r--r--lib/gitlab/ci/trace/http_io.rb197
-rw-r--r--lib/gitlab/git/repository.rb18
-rw-r--r--lib/gitlab/git_access.rb20
-rw-r--r--lib/gitlab/gitaly_client/conflicts_service.rb10
-rw-r--r--lib/gitlab/http_io.rb193
-rw-r--r--lib/gitlab/shell.rb44
-rw-r--r--lib/tasks/gettext.rake8
-rw-r--r--locale/gitlab.pot43
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb12
-rw-r--r--spec/features/admin/admin_settings_spec.rb10
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb13
-rw-r--r--spec/features/projects/environments/environments_spec.rb12
-rw-r--r--spec/helpers/time_helper_spec.rb10
-rw-r--r--spec/javascripts/diffs/components/changed_files_spec.js91
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js4
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js12
-rw-r--r--spec/javascripts/helpers/wait_for_promises.js1
-rw-r--r--spec/javascripts/job_spec.js27
-rw-r--r--spec/javascripts/smart_interval_spec.js201
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb24
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb70
-rw-r--r--spec/lib/gitlab/git_access_spec.rb24
-rw-r--r--spec/lib/gitlab/http_io_spec.rb (renamed from spec/lib/gitlab/ci/trace/http_io_spec.rb)43
-rw-r--r--spec/lib/gitlab/shell_spec.rb105
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb24
-rw-r--r--spec/mailers/notify_spec.rb11
-rw-r--r--spec/models/ci/build_spec.rb54
-rw-r--r--spec/models/repository_spec.rb22
-rw-r--r--spec/policies/environment_policy_spec.rb106
-rw-r--r--spec/requests/api/groups_spec.rb27
-rw-r--r--spec/requests/api/jobs_spec.rb8
-rw-r--r--spec/requests/api/namespaces_spec.rb12
-rw-r--r--spec/requests/api/projects_spec.rb14
-rw-r--r--spec/serializers/environment_entity_spec.rb3
-rw-r--r--spec/serializers/environment_serializer_spec.rb10
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb10
-rw-r--r--spec/support/helpers/test_env.rb4
-rw-r--r--spec/support/http_io/http_io_helpers.rb60
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb62
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb37
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')