diff options
152 files changed, 1966 insertions, 716 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f23981c84..248c85304a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.4.1 (2018-01-24) + +### Fixed (4 changes) + +- Ensure that users can reclaim a namespace or project path that is blocked by an orphaned route. !16242 +- Correctly escape UTF-8 path elements for uploads. !16560 +- Fix issues when rendering groups and their children. !16584 +- Fix bug in which projects with forks could not change visibility settings from Private to Public. !16595 + +### Performance (2 changes) + +- rework indexes on redirect_routes. +- Remove unecessary query from labels filter. + + ## 10.4.0 (2018-01-22) ### Security (8 changes, 1 of them is from the community) diff --git a/app/assets/images/illustrations/pending_job_empty.svg b/app/assets/images/illustrations/pending_job_empty.svg new file mode 100644 index 00000000000..8de695afa18 --- /dev/null +++ b/app/assets/images/illustrations/pending_job_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="430" height="200" viewBox="0 0 430 200"><g fill="none" fill-rule="evenodd"><g transform="translate(138 65)"><path fill="#E5E5E5" fill-rule="nonzero" d="M35 70a2 2 0 1 1 0-4c2.542 0 5.042-.305 7.463-.904a2 2 0 1 1 .96 3.884A35.075 35.075 0 0 1 35 70zm18.21-5.105a2 2 0 1 1-2.083-3.414 31.143 31.143 0 0 0 5.896-4.664 2 2 0 1 1 2.842 2.815 35.143 35.143 0 0 1-6.654 5.263zM66.106 51.06a2 2 0 0 1-3.552-1.838 30.77 30.77 0 0 0 2.612-7.042 2 2 0 1 1 3.892.922 34.77 34.77 0 0 1-2.952 7.958zm3.816-18.433a2 2 0 1 1-3.991.268 30.873 30.873 0 0 0-1.407-7.38 2 2 0 0 1 3.808-1.223 34.873 34.873 0 0 1 1.59 8.335zm-6.346-17.842a2 2 0 0 1-3.264 2.312 31.188 31.188 0 0 0-5.054-5.564 2 2 0 0 1 2.615-3.027 35.188 35.188 0 0 1 5.703 6.279zM48.895 2.867a2 2 0 0 1-1.59 3.67 30.758 30.758 0 0 0-7.206-2.12 2 2 0 1 1 .653-3.946 34.758 34.758 0 0 1 8.143 2.396zM30.263.318a2 2 0 0 1 .537 3.964c-2.505.339-4.94.98-7.266 1.907a2 2 0 1 1-1.48-3.716A34.774 34.774 0 0 1 30.263.318zM12.907 7.853a2 2 0 0 1 2.527 3.1 31.196 31.196 0 0 0-5.213 5.416 2 2 0 0 1-3.196-2.406 35.196 35.196 0 0 1 5.882-6.11zM1.99 23.343a2 2 0 0 1 3.772 1.331 30.82 30.82 0 0 0-1.619 7.337 2 2 0 1 1-3.982-.38 34.82 34.82 0 0 1 1.829-8.289zM.719 42.086a2 2 0 1 1 3.917-.806 30.757 30.757 0 0 0 2.4 7.118 2 2 0 1 1-3.605 1.73 34.757 34.757 0 0 1-2.713-8.042zM9.393 58.86a2 2 0 0 1 2.926-2.728 31.167 31.167 0 0 0 5.751 4.841 2 2 0 1 1-2.187 3.349 35.167 35.167 0 0 1-6.49-5.462zm16.245 9.873a2 2 0 1 1 1.067-3.855 30.979 30.979 0 0 0 7.434 1.11 2 2 0 1 1-.11 3.998 34.979 34.979 0 0 1-8.391-1.253z"/><circle cx="35" cy="35" r="16" stroke="#E1DBF1" stroke-width="4"/><path fill="#6B4FBB" d="M37 33h5a2 2 0 1 1 0 4h-7a2 2 0 0 1-2-2v-8a2 2 0 1 1 4 0v6z"/></g><g transform="translate(247 30)"><rect width="116" height="135" y="5" fill="#F9F9F9" rx="10"/><rect width="116" height="134" x="5" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="10"/><g transform="translate(23 23)"><rect width="16" height="4" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="32" y="12" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="44" fill="#EEE" rx="2"/><rect width="16" height="4" x="12" y="24" fill="#E1DBF1" rx="2"/><rect width="16" height="4" x="64" y="36" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="32" y="36" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="52" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="48" fill="#E1DBF1" rx="2"/><rect width="8" height="4" x="44" y="36" fill="#FC6D26" rx="2"/><rect width="4" height="4" x="56" y="36" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="64" y="60" fill="#E1DBF1" rx="2"/><rect width="4" height="4" x="72" y="60" fill="#FC6D26" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="28" height="4" y="36" fill="#EEE" rx="2"/><rect width="28" height="4" x="44" y="48" fill="#EEE" rx="2"/><rect width="28" height="4" x="32" y="60" fill="#EFEDF8" rx="2"/><rect width="28" height="4" y="12" fill="#6B4FBB" rx="2"/><rect width="28" height="4" x="32" y="24" fill="#C3B8E3" rx="2"/><rect width="8" height="4" y="24" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" y="48" fill="#6B4FBB" rx="2"/><rect width="12" height="4" y="48" fill="#FC6D26" rx="2"/><rect width="12" height="4" y="60" fill="#FEF0E8" rx="2"/><rect width="12" height="4" x="16" y="60" fill="#FEF0E8" rx="2"/></g><g transform="translate(23 95)"><rect width="16" height="4" fill="#EFEDF8" rx="2"/><rect width="16" height="4" x="18" y="12" fill="#FC6D26" rx="2"/><rect width="16" height="4" x="44" fill="#6B4FBB" rx="2"/><rect width="8" height="4" x="20" fill="#FEE1D3" rx="2"/><rect width="8" height="4" x="38" y="12" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="64" fill="#FEF0E8" rx="2"/><rect width="8" height="4" x="32" fill="#FC6D26" rx="2"/><rect width="14" height="4" y="12" fill="#EEE" rx="2"/></g></g><path fill="#FC6D26" fill-rule="nonzero" d="M81 119c-10.493 0-19-8.507-19-19s8.507-19 19-19 19 8.507 19 19-8.507 19-19 19zm0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15zm-5-20a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v6a2 2 0 1 1-4 0v-6a2 2 0 0 1 2-2z"/><path fill="#E5E5E5" fill-rule="nonzero" d="M108 102c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm93 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2zm14 0c0-1.105.887-2 1.998-2h4.004c1.103 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4.004a1.994 1.994 0 0 1-1.998-2z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 843564ce016..c6091efd62f 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -53,10 +53,10 @@ </i> </div> <div class="deploy-key-content key-list-item-info"> - <strong class="title"> + <strong class="title qa-key-title"> {{ deployKey.title }} </strong> - <div class="description"> + <div class="description qa-key-fingerprint"> {{ deployKey.fingerprint }} </div> </div> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 89fc002d4ce..262ed3783fb 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -89,6 +89,11 @@ import SearchAutocomplete from './search_autocomplete'; .then(callDefault) .catch(fail); break; + case 'projects:milestones:index': + import('./pages/projects/milestones/index') + .then(callDefault) + .catch(fail); + break; case 'projects:milestones:show': import('./pages/projects/milestones/show') .then(callDefault) diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index 0c3ce848e3d..c9a18353f2e 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,3 @@ -import initMilestonesShow from '~/pages/init_milestones_show'; +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; export default initMilestonesShow; diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue new file mode 100644 index 00000000000..c43e0a0490f --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -0,0 +1,110 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { n__, s__, sprintf } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; + + export default { + components: { + modal, + }, + props: { + issueCount: { + type: Number, + required: true, + }, + mergeRequestCount: { + type: Number, + required: true, + }, + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); + + if (this.issueCount === 0 && this.mergeRequestCount === 0) { + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project. +%{milestoneTitle} is not currently used in any issues or merge requests.`), + { + milestoneTitle, + }, + false, + ); + } + + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + + return axios.delete(this.milestoneUrl) + .then((response) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + + if (error.response && error.response.status === 404) { + Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); + } else { + Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); + } + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="delete-milestone-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="s__('Milestones|Delete milestone')" + @submit="onSubmit"> + + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> + </template> + + </modal> +</template> diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/pages/milestones/shared/index.js b/app/assets/javascripts/pages/milestones/shared/index.js new file mode 100644 index 00000000000..327e2cf569c --- /dev/null +++ b/app/assets/javascripts/pages/milestones/shared/index.js @@ -0,0 +1,88 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import deleteMilestoneModal from './components/delete_milestone_modal.vue'; +import eventHub from './event_hub'; + +export default () => { + Vue.use(Translate); + + const onRequestFinished = ({ milestoneUrl, successful }) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + + button.querySelector('.js-loading-icon').classList.add('hidden'); + }; + + const onRequestStarted = (milestoneUrl) => { + const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + button.setAttribute('disabled', ''); + button.querySelector('.js-loading-icon').classList.remove('hidden'); + eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + milestoneId: parseInt(button.dataset.milestoneId, 10), + milestoneTitle: button.dataset.milestoneTitle, + milestoneUrl: button.dataset.milestoneUrl, + issueCount: parseInt(button.dataset.milestoneIssueCount, 10), + mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10), + }; + eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted); + eventHub.$emit('deleteMilestoneModal.props', modalProps); + }; + + const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.addEventListener('click', onDeleteButtonClick); + } + + eventHub.$once('deleteMilestoneModal.mounted', () => { + for (let i = 0; i < deleteMilestoneButtons.length; i += 1) { + const button = deleteMilestoneButtons[i]; + button.removeAttribute('disabled'); + } + }); + + return new Vue({ + el: '#delete-milestone-modal', + components: { + deleteMilestoneModal, + }, + data() { + return { + modalProps: { + milestoneId: -1, + milestoneTitle: '', + milestoneUrl: '', + issueCount: -1, + mergeRequestCount: -1, + }, + }; + }, + mounted() { + eventHub.$on('deleteMilestoneModal.props', this.setModalProps); + eventHub.$emit('deleteMilestoneModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('deleteMilestoneModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement(deleteMilestoneModal, { + props: this.modalProps, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/init_milestones_show.js b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js index 7aa5be0d5b9..7aa5be0d5b9 100644 --- a/app/assets/javascripts/pages/init_milestones_show.js +++ b/app/assets/javascripts/pages/milestones/shared/init_milestones_show.js diff --git a/app/assets/javascripts/pages/projects/milestones/index/index.js b/app/assets/javascripts/pages/projects/milestones/index/index.js new file mode 100644 index 00000000000..8fb4d83d8a3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/index/index.js @@ -0,0 +1,3 @@ +import milestones from '~/pages/milestones/shared'; + +export default milestones; diff --git a/app/assets/javascripts/pages/projects/milestones/show/index.js b/app/assets/javascripts/pages/projects/milestones/show/index.js index 0c3ce848e3d..35b5c9c2ced 100644 --- a/app/assets/javascripts/pages/projects/milestones/show/index.js +++ b/app/assets/javascripts/pages/projects/milestones/show/index.js @@ -1,3 +1,7 @@ -import initMilestonesShow from '~/pages/init_milestones_show'; +import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import milestones from '~/pages/milestones/shared'; -export default initMilestonesShow; +export default () => { + initMilestonesShow(); + milestones(); +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index d48f3a01420..d174a900f63 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -2,7 +2,7 @@ import { getTimeago } from '~/lib/utils/datetime_utility'; import { visitUrl } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; -import StatusIcon from './mr_widget_status_icon'; +import StatusIcon from './mr_widget_status_icon.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js deleted file mode 100644 index eeb990908f6..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js +++ /dev/null @@ -1,36 +0,0 @@ -import ciIcon from '../../vue_shared/components/ci_icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - status: { type: String, required: true }, - showDisabledButton: { type: Boolean, required: false }, - }, - components: { - ciIcon, - loadingIcon, - }, - computed: { - statusObj() { - return { - group: this.status, - icon: `status_${this.status}`, - }; - }, - }, - template: ` - <div class="space-children flex-container-block append-right-10"> - <div v-if="status === 'loading'" class="mr-widget-icon"> - <loading-icon /> - </div> - <ci-icon v-else :status="statusObj" /> - <button - v-if="showDisabledButton" - type="button" - class="js-disabled-merge-button btn btn-success btn-sm" - disabled="true"> - Merge - </button> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue new file mode 100644 index 00000000000..1fdc3218671 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -0,0 +1,57 @@ +<script> + import ciIcon from '../../vue_shared/components/ci_icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + ciIcon, + loadingIcon, + }, + props: { + status: { + type: String, + required: true, + }, + showDisabledButton: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isLoading() { + return this.status === 'loading'; + }, + statusObj() { + return { + group: this.status, + icon: `status_${this.status}`, + }; + }, + }, + }; +</script> +<template> + <div class="space-children flex-container-block append-right-10"> + <div + v-if="isLoading" + class="mr-widget-icon" + > + <loading-icon /> + </div> + + <ci-icon + v-else + :status="statusObj" + /> + + <button + v-if="showDisabledButton" + type="button" + class="js-disabled-merge-button btn btn-success btn-sm" + disabled="true" + > + {{ s__("mrWidget|Merge") }} + </button> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue index afa9cc57544..cfbd44d41b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -1,5 +1,5 @@ <script> - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetArchived', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 77dd243d617..40c3cb500bb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -1,7 +1,7 @@ <script> import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import eventHub from '../../event_hub'; - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetAutoMergeFailed', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js deleted file mode 100644 index 09561694939..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js +++ /dev/null @@ -1,18 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetChecking', - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="loading" :show-disabled-button="true" /> - <div class="media-body space-children"> - <span class="bold"> - Checking ability to merge automatically - </span> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue new file mode 100644 index 00000000000..caeaac75b45 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -0,0 +1,23 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetChecking', + components: { + statusIcon, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="loading" + :show-disabled-button="true" + /> + <div class="media-body space-children"> + <span class="bold"> + {{ s__("mrWidget|Checking ability to merge automatically") }} + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js deleted file mode 100644 index dc19b20aa11..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js +++ /dev/null @@ -1,35 +0,0 @@ -import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetClosed', - props: { - mr: { type: Object, required: true }, - }, - components: { - 'mr-widget-author-and-time': mrWidgetAuthorTime, - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon status="warning" /> - <div class="media-body"> - <mr-widget-author-and-time - actionText="Closed by" - :author="mr.metrics.closedBy" - :dateTitle="mr.metrics.closedAt" - :dateReadable="mr.metrics.readableClosedAt" - /> - <section class="mr-info-list"> - <p> - The changes were not merged into - <a - :href="mr.targetBranchPath" - class="label-branch"> - {{mr.targetBranch}}</a> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue new file mode 100644 index 00000000000..71bfdaf801e --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -0,0 +1,48 @@ +<script> + import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetClosed', + components: { + mrWidgetAuthorTime, + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs metrics and targetBranch */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + /> + <div class="media-body"> + <mr-widget-author-time + :action-text="s__('mrWidget|Closed by')" + :author="mr.metrics.closedBy" + :date-title="mr.metrics.closedAt" + :date-readable="mr.metrics.readableClosedAt" + /> + + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes were not merged into") }} + <a + :href="mr.targetBranchPath" + class="label-branch" + > + {{ mr.targetBranch }} + </a> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js deleted file mode 100644 index 7a887bacfa7..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js +++ /dev/null @@ -1,47 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetConflicts', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body media"> - <status-icon - status="warning" - :show-disabled-button="true" /> - <div class="media-body space-children"> - <span - v-if="mr.shouldBeRebased" - class="bold"> - Fast-forward merge is not possible. - To merge this request, first rebase locally. - </span> - <template v-else> - <span class="bold"> - There are merge conflicts<span v-if="!mr.canMerge">.</span> - <span v-if="!mr.canMerge"> - Resolve these conflicts or ask someone with write access to this repository to merge it locally - </span> - </span> - <a - v-if="mr.canMerge && mr.conflictResolutionPath" - :href="mr.conflictResolutionPath" - class="js-resolve-conflicts-button btn btn-default btn-xs"> - Resolve conflicts - </a> - <a - v-if="mr.canMerge" - class="js-merge-locally-button btn btn-default btn-xs" - data-toggle="modal" - href="#modal_merge_info"> - Merge locally - </a> - </template> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue new file mode 100644 index 00000000000..dad4b0fe49d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -0,0 +1,61 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetConflicts', + components: { + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it + only needs a few props */ + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body media"> + <status-icon + status="warning" + :show-disabled-button="true" + /> + + <div class="media-body space-children"> + <span + v-if="mr.shouldBeRebased" + class="bold" + > + {{ s__(`mrWidget|Fast-forward merge is not possible. +To merge this request, first rebase locally.`) }} + </span> + <template v-else> + <span class="bold"> + {{ s__("mrWidget|There are merge conflicts") }}<span v-if="!mr.canMerge">.</span> + <span v-if="!mr.canMerge"> + {{ s__(`mrWidget|Resolve these conflicts or ask someone + with write access to this repository to merge it locally`) }} + </span> + </span> + <a + v-if="mr.canMerge && mr.conflictResolutionPath" + :href="mr.conflictResolutionPath" + class="js-resolve-conflicts-button btn btn-default btn-xs" + > + {{ s__("mrWidget|Resolve conflicts") }} + </a> + <button + v-if="mr.canMerge" + class="js-merge-locally-button btn btn-default btn-xs" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ s__("mrWidget|Merge locally") }} + </button> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js index fc5f18695b7..76b0235af1b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js index bd349111bbd..357485b9e78 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js @@ -1,5 +1,5 @@ import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import MRWidgetAuthor from '../../components/mr_widget_author'; import eventHub from '../../event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js index ba9681680ef..7f8d78cab73 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js @@ -2,7 +2,7 @@ import Flash from '../../../flash'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import tooltip from '../../../vue_shared/directives/tooltip'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js deleted file mode 100644 index f6d1a4feeb2..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js +++ /dev/null @@ -1,29 +0,0 @@ -import statusIcon from '../mr_widget_status_icon'; - -export default { - name: 'MRWidgetMerging', - props: { - mr: { type: Object, required: true }, - }, - components: { - statusIcon, - }, - template: ` - <div class="mr-widget-body mr-state-locked media"> - <status-icon status="loading" /> - <div class="media-body"> - <h4> - This merge request is in the process of being merged - </h4> - <section class="mr-info-list"> - <p> - The changes will be merged into - <span class="label-branch"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> - </span> - </p> - </section> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue new file mode 100644 index 00000000000..953ddf40a51 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -0,0 +1,35 @@ +<script> + import statusIcon from '../mr_widget_status_icon.vue'; + + export default { + name: 'MRWidgetMerging', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), + }, + }, + }; +</script> +<template> + <div class="mr-widget-body mr-state-locked media"> + <status-icon status="loading" /> + <div class="media-body"> + <h4> + {{ s__("mrWidget|This merge request is in the process of being merged") }} + </h4> + <section class="mr-info-list"> + <p> + {{ s__("mrWidget|The changes will be merged into") }} + <span class="label-branch"> + <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> + </span> + </p> + </section> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js index 16ff1109e3f..303877d6fbf 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import mrWidgetMergeHelp from '../../components/mr_widget_merge_help'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js index 00047718201..cea3d97fa88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetNotAllowed', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js index 2c84f423ee2..e66ce071ab4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js index cbaa73deffa..4d9a2ca530f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetPipelineBlocked', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index e51eef07093..7ba6c29006a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -3,7 +3,7 @@ import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 52dd0245ff0..2968af0d5cb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,7 +1,7 @@ <script> import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Flash from '../../../flash'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js index 46687cc85e1..142ddf477f1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetSHAMismatch', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js index 97b1940f4be..67b271c69ca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetUnresolvedDiscussions', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index b4b0f00445c..bbca641f65e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -1,4 +1,4 @@ -import statusIcon from '../mr_widget_status_icon'; +import statusIcon from '../mr_widget_status_icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 2dd3b2c2f98..8651945a3da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -18,11 +18,11 @@ export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; export { default as MergedState } from './components/states/mr_widget_merged'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge'; -export { default as ClosedState } from './components/states/mr_widget_closed'; -export { default as MergingState } from './components/states/mr_widget_merging'; +export { default as ClosedState } from './components/states/mr_widget_closed.vue'; +export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as WipState } from './components/states/mr_widget_wip'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts'; +export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed'; @@ -34,7 +34,7 @@ export { default as PipelineFailedState } from './components/states/mr_widget_pi export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds'; export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; -export { default as CheckingState } from './components/states/mr_widget_checking'; +export { default as CheckingState } from './components/states/mr_widget_checking.vue'; export { default as MRWidgetStore } from './stores/mr_widget_store'; export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index fd5c3c81a53..2d015ef086b 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -20,7 +20,7 @@ width: 100%; } - $image-widths: 250 306 394; + $image-widths: 250 306 394 430; @each $width in $image-widths { &.svg-#{$width} { img, diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 51ae09777fd..32b9894ae04 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -9,6 +9,7 @@ .modal-body { background-color: $modal-body-bg; + line-height: $line-height-base; min-height: $modal-body-height; position: relative; padding: #{3 * $grid-size} #{2 * $grid-size}; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0cba223c6a6..aeaa33bd3bd 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -310,8 +310,8 @@ } &.invalid { - @include status-color($gray-dark, $gray, $common-gray-dark); - border-color: $common-gray-light; + @include status-color($gray-dark, $gray, $gray-darkest); + border-color: $gray-darkest; } } @@ -335,8 +335,8 @@ &.invalid { svg { - border: 1px solid $common-gray-light; - fill: $common-gray-light; + border: 1px solid $gray-darkest; + fill: $gray-darkest; } } diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index a931b456a93..16abf7bab7e 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -8,7 +8,8 @@ class HealthController < ActionController::Base Gitlab::HealthChecks::Redis::CacheCheck, Gitlab::HealthChecks::Redis::QueuesCheck, Gitlab::HealthChecks::Redis::SharedStateCheck, - Gitlab::HealthChecks::FsShardsCheck + Gitlab::HealthChecks::FsShardsCheck, + Gitlab::HealthChecks::GitalyCheck ].freeze def readiness diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 0f70efbce40..75b17d05e22 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -83,7 +83,7 @@ class Projects::MilestonesController < Projects::ApplicationController Milestones::DestroyService.new(project, current_user).execute(milestone) respond_to do |format| - format.html { redirect_to namespace_project_milestones_path, status: 302 } + format.html { redirect_to namespace_project_milestones_path, status: 303 } format.js { head :ok } end end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index f4310ca2f06..d72457efec0 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -14,13 +14,13 @@ module AutoDevopsHelper if missing_service params = { - kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes')) + kubernetes: link_to('Kubernetes cluster', project_clusters_path(project)) } if missing_domain - _('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params + _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params else - _('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params + _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params end elsif missing_domain _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') 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 b75dab0acc5..8db7727b80c 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -8,7 +8,7 @@ = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe %li = _("Specify the following URL during the Runner setup:") - %code= root_url(only_path: false) + %code#coordinator_address= root_url(only_path: false) %li = _("Use the following registration token during setup:") %code#registration_token= registration_token diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 0f773933ac2..5c76d2d8f51 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -13,11 +13,11 @@ - if @user.avatar? You can change your avatar here - if gravatar_enabled? - or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} + or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} - else You can upload an avatar here - if gravatar_enabled? - or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} + or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} .col-lg-8 .clearfix.avatar-image.append-bottom-default = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index d0a380516f9..93407956f56 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -4,6 +4,7 @@ - branch_label = s_('ChangeTypeActionLabel|Revert in branch') - revert_merge_request = _('Revert this merge request') - revert_commit = _('Revert this commit') + - description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.') - title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit - when 'cherry-pick' - label = s_('ChangeTypeAction|Cherry-pick') @@ -17,6 +18,8 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title .modal-body + - if description + %p.append-bottom-20= description = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'start_branch', branch_label, class: 'control-label' diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml index 80eca96f7ce..d7bf2dc0cb6 100644 --- a/app/views/projects/commit/_other_user_signature_badge.html.haml +++ b/app/views/projects/commit/_other_user_signature_badge.html.haml @@ -1,6 +1,6 @@ - title = capture do This commit was signed with a different user's verified signature. -- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true } +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml index e737de48e22..22ffd66ff8e 100644 --- a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml +++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml @@ -2,6 +2,6 @@ This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user. -- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true } +- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 44aa8002f12..aac020b42c5 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -10,7 +10,7 @@ - title = capture do .gpg-popover-status .gpg-popover-icon{ class: css_class } - = render "shared/icons/#{icon}.svg" + = sprite_icon(icon) %div = title diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml index 1af58027b83..00e1efe0582 100644 --- a/app/views/projects/commit/_unverified_signature_badge.html.haml +++ b/app/views/projects/commit/_unverified_signature_badge.html.haml @@ -1,6 +1,6 @@ - title = capture do This commit was signed with an <strong>unverified</strong> signature. -- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' } +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'status_notfound_borderless' } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml index 423beba2120..31408806be7 100644 --- a/app/views/projects/commit/_verified_signature_badge.html.haml +++ b/app/views/projects/commit/_verified_signature_badge.html.haml @@ -2,6 +2,6 @@ This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user. -- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true } +- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'status_success_borderless', show_user: true } = render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index eb0773f2d4e..93efa7e8e86 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -103,8 +103,8 @@ content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered') - else = render 'empty_state', - illustration: 'illustrations/job_not_triggered.svg', - illustration_size: 'svg-306', + illustration: 'illustrations/pending_job_empty.svg', + illustration_size: 'svg-430', title: _('This job has not started yet'), content: _('This job is in pending state and is waiting to be picked by a runner') = render "sidebar" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index fcbf7cb802b..6a7bc4b1888 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -12,6 +12,8 @@ New milestone .milestones + #delete-milestone-modal + %ul.content-list = render @milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 5dd4d2c949c..623c42ba88e 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -35,8 +35,18 @@ - else = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to project_milestone_path(@project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do - Delete + %button.js-delete-milestone-button.btn.btn-grouped.btn-danger{ data: { toggle: 'modal', + target: '#delete-milestone-modal', + milestone_id: @milestone.id, + milestone_title: markdown_field(@milestone, :title), + milestone_url: project_milestone_path(@project, @milestone), + milestone_issue_count: @milestone.issues.count, + milestone_merge_request_count: @milestone.merge_requests.count }, + disabled: true } + = _('Delete') + = icon('spin spinner', class: 'js-loading-icon hidden' ) + + #delete-milestone-modal %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') diff --git a/app/views/shared/icons/_icon_status_notfound_borderless.svg b/app/views/shared/icons/_icon_status_notfound_borderless.svg deleted file mode 100644 index e58bd264ef8..00000000000 --- a/app/views/shared/icons/_icon_status_notfound_borderless.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0V11.78a5.9 5.9 0 0 0 .827-.492z" fill-rule="nonzero"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></svg> diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg deleted file mode 100644 index 8ee5be7ab78..00000000000 --- a/app/views/shared/icons/_icon_status_success_borderless.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg> diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 50f4901a2dd..e08a49b4e59 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -56,6 +56,13 @@ = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" - = link_to project_milestone_path(milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do - Delete - + %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal', + target: '#delete-milestone-modal', + milestone_id: milestone.id, + milestone_title: markdown_field(milestone, :title), + milestone_url: project_milestone_path(milestone.project, milestone), + milestone_issue_count: milestone.issues.count, + milestone_merge_request_count: milestone.merge_requests.count }, + disabled: true } + = _('Delete') + = icon('spin spinner', class: 'js-loading-icon hidden' ) diff --git a/changelogs/unreleased/39917-revert-this-merge-request-text.yml b/changelogs/unreleased/39917-revert-this-merge-request-text.yml new file mode 100644 index 00000000000..9a27be1f9c6 --- /dev/null +++ b/changelogs/unreleased/39917-revert-this-merge-request-text.yml @@ -0,0 +1,5 @@ +--- +title: Changes Revert this merge request text +merge_request: 16611 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml b/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml deleted file mode 100644 index 96bb59d303c..00000000000 --- a/changelogs/unreleased/40612-cannot-change-project-visibility-from-private-even-when-owner.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix bug in which projects with forks could not change visibility settings from - Private to Public -merge_request: 16595 -author: -type: fixed diff --git a/changelogs/unreleased/42053-link-to-clusters-in-auto-devops-instead-of-kubernetes-service.yml b/changelogs/unreleased/42053-link-to-clusters-in-auto-devops-instead-of-kubernetes-service.yml new file mode 100644 index 00000000000..5cb5dc3ccd8 --- /dev/null +++ b/changelogs/unreleased/42053-link-to-clusters-in-auto-devops-instead-of-kubernetes-service.yml @@ -0,0 +1,5 @@ +--- +title: Link Auto DevOps settings to Clusters page +merge_request: 16641 +author: +type: changed diff --git a/changelogs/unreleased/42159-utf8-uploads.yml b/changelogs/unreleased/42159-utf8-uploads.yml deleted file mode 100644 index f6eba8f28f5..00000000000 --- a/changelogs/unreleased/42159-utf8-uploads.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correctly escape UTF-8 path elements for uploads -merge_request: 16560 -author: -type: fixed diff --git a/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml b/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml new file mode 100644 index 00000000000..c64bee9126e --- /dev/null +++ b/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml @@ -0,0 +1,5 @@ +--- +title: Fix encoding issue when counting commit count +merge_request: 16637 +author: +type: fixed diff --git a/changelogs/unreleased/42220-add-pending-empty-state.yml b/changelogs/unreleased/42220-add-pending-empty-state.yml new file mode 100644 index 00000000000..ad39578f2d9 --- /dev/null +++ b/changelogs/unreleased/42220-add-pending-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Adds empty state illustration for pending job +merge_request: +author: +type: other diff --git a/changelogs/unreleased/42285-not-found-status-icon.yml b/changelogs/unreleased/42285-not-found-status-icon.yml new file mode 100644 index 00000000000..ea7ff9d6ae7 --- /dev/null +++ b/changelogs/unreleased/42285-not-found-status-icon.yml @@ -0,0 +1,5 @@ +--- +title: Replace verified badge icons and uniform colors +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/bvl-parent-preloading.yml b/changelogs/unreleased/bvl-parent-preloading.yml deleted file mode 100644 index 97c7bbb2a2a..00000000000 --- a/changelogs/unreleased/bvl-parent-preloading.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix issues when rendering groups and their children -merge_request: 16584 -author: -type: fixed diff --git a/changelogs/unreleased/default-to-https-for-gravatar-urls.yml b/changelogs/unreleased/default-to-https-for-gravatar-urls.yml new file mode 100644 index 00000000000..544c34fe31d --- /dev/null +++ b/changelogs/unreleased/default-to-https-for-gravatar-urls.yml @@ -0,0 +1,5 @@ +--- +title: Default to HTTPS for all Gravatar URLs +merge_request: 16666 +author: +type: fixed diff --git a/changelogs/unreleased/fix-redirect-routes-schema.yml b/changelogs/unreleased/fix-redirect-routes-schema.yml deleted file mode 100644 index ea2b916307a..00000000000 --- a/changelogs/unreleased/fix-redirect-routes-schema.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: rework indexes on redirect_routes -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/fl-mr-widget-refactor.yml b/changelogs/unreleased/fl-mr-widget-refactor.yml new file mode 100644 index 00000000000..d59cca68409 --- /dev/null +++ b/changelogs/unreleased/fl-mr-widget-refactor.yml @@ -0,0 +1,5 @@ +--- +title: Refactors mr widget components into vue files and adds i18n +merge_request: +author: +type: other diff --git a/changelogs/unreleased/issue_37143_2.yml b/changelogs/unreleased/issue_37143_2.yml deleted file mode 100644 index 38125f666b2..00000000000 --- a/changelogs/unreleased/issue_37143_2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove unecessary query from labels filter -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml b/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml deleted file mode 100644 index 55ab318df7d..00000000000 --- a/changelogs/unreleased/mk-delete-orphaned-routes-before-validation.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Ensure that users can reclaim a namespace or project path that is blocked by - an orphaned route -merge_request: 16242 -author: -type: fixed diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml deleted file mode 100644 index 3854985e576..00000000000 --- a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Return more consistent values for merge_status on MR APIs -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-add-gitaly-health-check.yml b/changelogs/unreleased/sh-add-gitaly-health-check.yml new file mode 100644 index 00000000000..32c4c5362b4 --- /dev/null +++ b/changelogs/unreleased/sh-add-gitaly-health-check.yml @@ -0,0 +1,5 @@ +--- +title: Add a gRPC health check to ensure Gitaly is up +merge_request: +author: +type: added diff --git a/changelogs/unreleased/winh-delete-milestone-modal.yml b/changelogs/unreleased/winh-delete-milestone-modal.yml new file mode 100644 index 00000000000..6517fbd5f63 --- /dev/null +++ b/changelogs/unreleased/winh-delete-milestone-modal.yml @@ -0,0 +1,5 @@ +--- +title: Add modal for deleting a milestone +merge_request: 16229 +author: +type: other diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f2f05b3eeb2..238e1583770 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -175,10 +175,12 @@ production: &base host: 'https://mattermost.example.com' ## Gravatar - ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html + ## If using gravatar.com, there's nothing to change here. For Libravatar + ## you'll need to provide the custom URLs. For more information, + ## see: https://docs.gitlab.com/ee/customization/libravatar.html gravatar: - # gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username} - # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon + # Gravatar/Libravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username} + # plain_url: "http://..." # default: https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon ## Auxiliary jobs diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index abc992e49dc..899e612ffbd 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -350,7 +350,7 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled # Settings['gravatar'] ||= Settingslogic.new({}) Settings.gravatar['enabled'] = true if Settings.gravatar['enabled'].nil? -Settings.gravatar['plain_url'] ||= 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' +Settings.gravatar['plain_url'] ||= 'https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon' Settings.gravatar['host'] = Settings.host_without_www(Settings.gravatar['plain_url']) diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb index 1cfb3bcb4bd..c0d45caec42 100644 --- a/config/initializers/rugged_use_gitlab_git_attributes.rb +++ b/config/initializers/rugged_use_gitlab_git_attributes.rb @@ -7,7 +7,7 @@ # repository-wide language statistics: # <https://github.com/github/linguist/blob/v4.7.0/lib/linguist/lazy_blob.rb#L33-L36> # -# The options passed by Linguist are those assumed by Gitlab::Git::Attributes +# The options passed by Linguist are those assumed by Gitlab::Git::InfoAttributes # anyway, and there is no great efficiency gain from just fetching the listed # attributes with our implementation, so we ignore the additional arguments. # @@ -19,7 +19,7 @@ module Rugged end def attributes - @attributes ||= Gitlab::Git::Attributes.new(path) + @attributes ||= Gitlab::Git::InfoAttributes.new(path) end end diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 764ee0ca72c..144cd4c26b0 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -66,9 +66,8 @@ To make full use of Auto DevOps, you will need: a domain configured with wildcard DNS which is gonna be used by all of your Auto DevOps applications. [Read the specifics](#auto-devops-base-domain). 1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) - - To enable deployments, you will need Kubernetes 1.5+. The [Kubernetes service][kubernetes-service] - integration will need to be enabled for the project, or enabled as a - [default service template](../../user/project/integrations/services_templates.md) + To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters] + for the project, or a Kubernetes [default service template](../../user/project/integrations/services_templates.md) for the entire GitLab installation. 1. **A load balancer** - You can use NGINX ingress by deploying it to your Kubernetes cluster using the @@ -587,7 +586,7 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ ``` [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 -[kubernetes-service]: ../../user/project/integrations/kubernetes.md +[kubernetes-clusters]: ../../user/project/clusters/index.md [docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor [review-app]: ../../ci/review_apps/index.md [container-registry]: ../../user/project/container_registry.md diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature index 1af05b3c326..d121222308d 100644 --- a/features/project/issues/milestones.feature +++ b/features/project/issues/milestones.feature @@ -18,12 +18,15 @@ Feature: Project Issues Milestones Given I click link "New Milestone" And I submit new milestone "v2.3" Then I should see milestone "v2.3" - Given I click link to remove milestone + Given I click button to remove milestone + And I confirm in modal When I visit project "Shop" activity page Then I should see deleted milestone activity + @javascript Scenario: I delete new milestone - Given I click link to remove milestone + Given I click button to remove milestone + And I confirm in modal And I should see no milestones @javascript diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index 33a24e8913a..4ce67aa651c 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -3,7 +3,6 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps include SharedProject include SharedPaths include SharedMarkdown - include CapybaraHelpers step 'I should see milestone "v2.2"' do milestone = @project.milestones.find_by(title: "v2.2") @@ -65,8 +64,12 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps expect(page).to have_selector('#tab-issues li.issuable-row', count: 4) end - step 'I click link to remove milestone' do - confirm_modal_if_present { click_link 'Delete' } + step 'I click button to remove milestone' do + click_button 'Delete' + end + + step 'I confirm in modal' do + click_button 'Delete milestone' end step 'I should see no milestones' do diff --git a/features/support/capybara_helpers.rb b/features/support/capybara_helpers.rb deleted file mode 100644 index 647f8d087c3..00000000000 --- a/features/support/capybara_helpers.rb +++ /dev/null @@ -1,10 +0,0 @@ -module CapybaraHelpers - def confirm_modal_if_present - if Capybara.current_driver == Capybara.javascript_driver - accept_confirm { yield } - return - end - - yield - end -end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5b470bd3479..7b9a80a234b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -507,16 +507,7 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_pipeline_succeeds - - # Ideally we should deprecate `MergeRequest#merge_status` exposure and - # use `MergeRequest#mergeable?` instead (boolean). - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more - # information. - expose :merge_status do |merge_request| - # In order to avoid having a breaking change for users, we keep returning the - # expected values from MergeRequest#merge_status state machine. - merge_request.mergeable? ? 'can_be_merged' : 'cannot_be_merged' - end + expose :merge_status expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :user_notes_count diff --git a/lib/gitlab/git/attributes_at_ref_parser.rb b/lib/gitlab/git/attributes_at_ref_parser.rb new file mode 100644 index 00000000000..26b5bd520d5 --- /dev/null +++ b/lib/gitlab/git/attributes_at_ref_parser.rb @@ -0,0 +1,14 @@ +module Gitlab + module Git + # Parses root .gitattributes file at a given ref + class AttributesAtRefParser + delegate :attributes, to: :@parser + + def initialize(repository, ref) + blob = repository.blob_at(ref, '.gitattributes') + + @parser = AttributesParser.new(blob&.data) + end + end + end +end diff --git a/lib/gitlab/git/attributes.rb b/lib/gitlab/git/attributes_parser.rb index 2d20cd473a7..d8aeabb6cba 100644 --- a/lib/gitlab/git/attributes.rb +++ b/lib/gitlab/git/attributes_parser.rb @@ -1,42 +1,26 @@ -# Gitaly note: JV: not sure what to make of this class. Why does it use -# the full disk path of the repository to look up attributes This is -# problematic in Gitaly, because Gitaly hides the full disk path to the -# repository from gitlab-ce. - module Gitlab module Git # Class for parsing Git attribute files and extracting the attributes for # file patterns. - # - # Unlike Rugged this parser only needs a single IO call (a call to `open`), - # vastly reducing the time spent in extracting attributes. - # - # This class _only_ supports parsing the attributes file located at - # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files - # (`.gitattributes` is copied to this particular path). - # - # Basic usage: - # - # attributes = Gitlab::Git::Attributes.new(some_repo.path) - # - # attributes.attributes('README.md') # => { "eol" => "lf } - class Attributes - # path - The path to the Git repository. - def initialize(path) - @path = File.expand_path(path) - @patterns = nil + class AttributesParser + def initialize(attributes_data) + @data = attributes_data || "" + + if @data.is_a?(File) + @patterns = parse_file + end end # Returns all the Git attributes for the given path. # - # path - A path to a file for which to get the attributes. + # file_path - A path to a file for which to get the attributes. # # Returns a Hash. - def attributes(path) - full_path = File.join(@path, path) + def attributes(file_path) + absolute_path = File.join('/', file_path) patterns.each do |pattern, attrs| - return attrs if File.fnmatch?(pattern, full_path) + return attrs if File.fnmatch?(pattern, absolute_path) end {} @@ -98,16 +82,10 @@ module Gitlab # Iterates over every line in the attributes file. def each_line - full_path = File.join(@path, 'info/attributes') + @data.each_line do |line| + break unless line.valid_encoding? - return unless File.exist?(full_path) - - File.open(full_path, 'r') do |handle| - handle.each_line do |line| - break unless line.valid_encoding? - - yield line.strip - end + yield line.strip end end @@ -125,7 +103,8 @@ module Gitlab parsed = attrs ? parse_attributes(attrs) : {} - pairs << [File.join(@path, pattern), parsed] + absolute_pattern = File.join('/', pattern) + pairs << [absolute_pattern, parsed] end # Newer entries take precedence over older entries. diff --git a/lib/gitlab/git/info_attributes.rb b/lib/gitlab/git/info_attributes.rb new file mode 100644 index 00000000000..e79a440950b --- /dev/null +++ b/lib/gitlab/git/info_attributes.rb @@ -0,0 +1,49 @@ +# Gitaly note: JV: not sure what to make of this class. Why does it use +# the full disk path of the repository to look up attributes This is +# problematic in Gitaly, because Gitaly hides the full disk path to the +# repository from gitlab-ce. + +module Gitlab + module Git + # Parses gitattributes at `$GIT_DIR/info/attributes` + # + # Unlike Rugged this parser only needs a single IO call (a call to `open`), + # vastly reducing the time spent in extracting attributes. + # + # This class _only_ supports parsing the attributes file located at + # `$GIT_DIR/info/attributes` as GitLab doesn't use any other files + # (`.gitattributes` is copied to this particular path). + # + # Basic usage: + # + # attributes = Gitlab::Git::InfoAttributes.new(some_repo.path) + # + # attributes.attributes('README.md') # => { "eol" => "lf } + class InfoAttributes + delegate :attributes, :patterns, to: :parser + + # path - The path to the Git repository. + def initialize(path) + @repo_path = File.expand_path(path) + end + + def parser + @parser ||= begin + if File.exist?(attributes_path) + File.open(attributes_path, 'r') do |file_handle| + AttributesParser.new(file_handle) + end + else + AttributesParser.new("") + end + end + end + + private + + def attributes_path + @attributes_path ||= File.join(@repo_path, 'info/attributes') + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d7c712e75c5..d6c0980255f 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -102,7 +102,7 @@ module Gitlab ) @path = File.join(storage_path, @relative_path) @name = @relative_path.split("/").last - @attributes = Gitlab::Git::Attributes.new(path) + @attributes = Gitlab::Git::InfoAttributes.new(path) end def ==(other) @@ -490,7 +490,11 @@ module Gitlab return [] end - log_by_shell(sha, options) + if log_using_shell?(options) + log_by_shell(sha, options) + else + log_by_walk(sha, options) + end end def count_commits(options) @@ -991,6 +995,18 @@ module Gitlab attributes(path)[name] end + # Check .gitattributes for a given ref + # + # This only checks the root .gitattributes file, + # it does not traverse subfolders to find additional .gitattributes files + # + # This method is around 30 times slower than `attributes`, + # which uses `$GIT_DIR/info/attributes` + def attributes_at(ref, file_path) + parser = AttributesAtRefParser.new(self, ref) + parser.attributes(file_path) + end + def languages(ref = nil) Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled| if is_enabled @@ -1517,6 +1533,27 @@ module Gitlab end end + def log_using_shell?(options) + options[:path].present? || + options[:disable_walk] || + options[:skip_merges] || + options[:after] || + options[:before] + end + + def log_by_walk(sha, options) + walk_options = { + show: sha, + sort: Rugged::SORT_NONE, + limit: options[:limit], + offset: options[:offset] + } + Rugged::Walker.walk(rugged, walk_options).to_a + end + + # Gitaly note: JV: although #log_by_shell shells out to Git I think the + # complexity is such that we should migrate it as Ruby before trying to + # do it in Go. def log_by_shell(sha, options) limit = options[:limit].to_i offset = options[:offset].to_i diff --git a/lib/gitlab/git/wiki_page.rb b/lib/gitlab/git/wiki_page.rb index a06bac4414f..669ae11a423 100644 --- a/lib/gitlab/git/wiki_page.rb +++ b/lib/gitlab/git/wiki_page.rb @@ -1,7 +1,7 @@ module Gitlab module Git class WikiPage - attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical + attr_reader :url_path, :title, :format, :path, :version, :raw_data, :name, :text_data, :historical, :formatted_data # This class is meant to be serializable so that it can be constructed # by Gitaly and sent over the network to GitLab. @@ -21,6 +21,7 @@ module Gitlab @raw_data = gollum_page.raw_data @name = gollum_page.name @historical = gollum_page.historical? + @formatted_data = gollum_page.formatted_data if gollum_page.is_a?(Gollum::Page) @version = version end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 4507ea923b4..6bd256f57c7 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -1,6 +1,8 @@ require 'base64' require 'gitaly' +require 'grpc/health/v1/health_pb' +require 'grpc/health/v1/health_services_pb' module Gitlab module GitalyClient @@ -69,14 +71,27 @@ module Gitlab @stubs ||= {} @stubs[storage] ||= {} @stubs[storage][name] ||= begin - klass = Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub) - addr = address(storage) - addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp' + klass = stub_class(name) + addr = stub_address(storage) klass.new(addr, :this_channel_is_insecure) end end end + def self.stub_class(name) + if name == :health_check + Grpc::Health::V1::Health::Stub + else + Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub) + end + end + + def self.stub_address(storage) + addr = address(storage) + addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp' + addr + end + def self.clear_stubs! MUTEX.synchronize do @stubs = nil diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 2231371cfa7..33a8d3e5612 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -125,11 +125,11 @@ module Gitlab def commit_count(ref, options = {}) request = Gitaly::CountCommitsRequest.new( repository: @gitaly_repo, - revision: ref + revision: encode_binary(ref) ) request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present? request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present? - request.path = options[:path] if options[:path].present? + request.path = encode_binary(options[:path]) if options[:path].present? request.max_count = options[:max_count] if options[:max_count].present? GitalyClient.call(@repository.storage, :commit_service, :count_commits, request, timeout: GitalyClient.medium_timeout).count diff --git a/lib/gitlab/gitaly_client/health_check_service.rb b/lib/gitlab/gitaly_client/health_check_service.rb new file mode 100644 index 00000000000..6c1213f5e20 --- /dev/null +++ b/lib/gitlab/gitaly_client/health_check_service.rb @@ -0,0 +1,19 @@ +module Gitlab + module GitalyClient + class HealthCheckService + def initialize(storage) + @storage = storage + end + + # Sends a gRPC health ping to the Gitaly server for the storage shard. + def check + request = Grpc::Health::V1::HealthCheckRequest.new + response = GitalyClient.call(@storage, :health_check, :check, request, timeout: GitalyClient.fast_timeout) + + { success: response&.status == :SERVING } + rescue GRPC::BadStatus => e + { success: false, message: e.to_s } + end + end + end +end diff --git a/lib/gitlab/health_checks/gitaly_check.rb b/lib/gitlab/health_checks/gitaly_check.rb new file mode 100644 index 00000000000..11416c002e3 --- /dev/null +++ b/lib/gitlab/health_checks/gitaly_check.rb @@ -0,0 +1,53 @@ +module Gitlab + module HealthChecks + class GitalyCheck + extend BaseAbstractCheck + + METRIC_PREFIX = 'gitaly_health_check'.freeze + + class << self + def readiness + repository_storages.map do |storage_name| + check(storage_name) + end + end + + def metrics + repository_storages.flat_map do |storage_name| + result, elapsed = with_timing { check(storage_name) } + labels = { shard: storage_name } + + [ + metric("#{metric_prefix}_success", successful?(result) ? 1 : 0, **labels), + metric("#{metric_prefix}_latency_seconds", elapsed, **labels) + ].flatten + end + end + + def check(storage_name) + serv = Gitlab::GitalyClient::HealthCheckService.new(storage_name) + result = serv.check + HealthChecks::Result.new(result[:success], result[:message], shard: storage_name) + end + + private + + def metric_prefix + METRIC_PREFIX + end + + def successful?(result) + result[:success] + end + + def repository_storages + storages.keys + end + + def storages + Gitlab.config.repositories.storages + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3ebc7859232..74d76caf47d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -186,13 +186,13 @@ msgstr "" msgid "Author" msgstr "" -msgid "Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly." +msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly." msgstr "" msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly." msgstr "" -msgid "Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly." +msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly." msgstr "" msgid "AutoDevOps|Auto DevOps (Beta)" diff --git a/package.json b/package.json index ef58f71ef8b..c68cf648932 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "worker-loader": "^1.1.0" }, "devDependencies": { - "@gitlab-org/gitlab-svgs": "^1.6.0", + "@gitlab-org/gitlab-svgs": "^1.7.0", "axios-mock-adapter": "^1.10.0", "babel-plugin-istanbul": "^4.1.5", "eslint": "^3.18.0", diff --git a/qa/Gemfile b/qa/Gemfile index d69c71003ae..c3e61568f3d 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -6,4 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18' gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.8.0' +gem 'net-ssh', require: false gem 'airborne', '~> 0.2.13' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 565adac7499..51d2e4d7a10 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -46,6 +46,7 @@ GEM mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.1) + net-ssh (4.1.0) netrc (0.11.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) @@ -97,6 +98,7 @@ DEPENDENCIES airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) + net-ssh pry-byebug (~> 3.5.1) rake (~> 12.3.0) rspec (~> 3.7) @@ -11,6 +11,7 @@ module QA autoload :Scenario, 'qa/runtime/scenario' autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' + autoload :RSAKey, 'qa/runtime/rsa_key' autoload :Address, 'qa/runtime/address' autoload :API, 'qa/runtime/api' end @@ -28,6 +29,7 @@ module QA autoload :Group, 'qa/factory/resource/group' autoload :Project, 'qa/factory/resource/project' autoload :DeployKey, 'qa/factory/resource/deploy_key' + autoload :Runner, 'qa/factory/resource/runner' autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' end @@ -49,7 +51,7 @@ module QA # autoload :Bootable, 'qa/scenario/bootable' autoload :Actable, 'qa/scenario/actable' - autoload :Entrypoint, 'qa/scenario/entrypoint' + autoload :Taggable, 'qa/scenario/taggable' autoload :Template, 'qa/scenario/template' ## @@ -108,7 +110,14 @@ module QA module Settings autoload :Common, 'qa/page/project/settings/common' autoload :Repository, 'qa/page/project/settings/repository' + autoload :CICD, 'qa/page/project/settings/ci_cd' autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' + autoload :Runners, 'qa/page/project/settings/runners' + end + + module Pipeline + autoload :Index, 'qa/page/project/pipeline/index' + autoload :Show, 'qa/page/project/pipeline/show' end end @@ -134,10 +143,13 @@ module QA end ## - # Classes describing shell interaction with GitLab + # Classes describing services being part of GitLab and how we can interact + # with these services, like through the shell. # - module Shell - autoload :Omnibus, 'qa/shell/omnibus' + module Service + autoload :Shellout, 'qa/service/shellout' + autoload :Omnibus, 'qa/service/omnibus' + autoload :Runner, 'qa/service/runner' end ## diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index 671114d38b1..ff0b4a46b77 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -10,6 +10,12 @@ module QA end end + product :fingerprint do + Page::Project::Settings::Repository.act do + expand_deploy_keys(&:key_fingerprint) + end + end + dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-to-deploy' project.description = 'project for adding deploy key test' @@ -19,7 +25,7 @@ module QA project.visit! Page::Menu::Side.act do - click_repository_setting + click_repository_settings end Page::Project::Settings::Repository.perform do |setting| diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb new file mode 100644 index 00000000000..5f37f8ac2e9 --- /dev/null +++ b/qa/qa/factory/resource/runner.rb @@ -0,0 +1,42 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class Runner < Factory::Base + attr_writer :name, :tags + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-with-ci-cd' + project.description = 'Project with CI/CD Pipelines' + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def fabricate! + project.visit! + + Page::Menu::Side.act { click_ci_cd_settings } + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.register! + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index ea4c920c82c..81ba80cdbaf 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -41,7 +41,21 @@ module QA end def click_element(name) - find(Page::Element.new(name).selector_css).click + find_element(name).click + end + + def find_element(name) + find(element_selector_css(name)) + end + + def within_element(name) + page.within(element_selector_css(name)) do + yield + end + end + + def element_selector_css(name) + Page::Element.new(name).selector_css end def self.path diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb index 1df4e0c2429..7f0f924c5e8 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/menu/side.rb @@ -5,18 +5,35 @@ module QA view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :repository_link, "title: 'Repository'" + element :pipelines_settings_link, "title: 'CI / CD'" element :top_level_items, '.sidebar-top-level-items' end - def click_repository_setting - hover_setting do - click_link('Repository') + def click_repository_settings + hover_settings do + within_submenu do + click_link('Repository') + end + end + end + + def click_ci_cd_settings + hover_settings do + within_submenu do + click_link('CI / CD') + end + end + end + + def click_ci_cd_pipelines + within_sidebar do + click_link('CI / CD') end end private - def hover_setting + def hover_settings within_sidebar do find('.qa-settings-item').hover @@ -29,6 +46,12 @@ module QA yield end end + + def within_submenu + page.within('.fly-out-list') do + yield + end + end end end end diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb new file mode 100644 index 00000000000..32c108393b9 --- /dev/null +++ b/qa/qa/page/project/pipeline/index.rb @@ -0,0 +1,13 @@ +module QA::Page + module Project::Pipeline + class Index < QA::Page::Base + view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do + element :pipeline_link, 'class="js-pipeline-url-link"' + end + + def go_to_latest_pipeline + first('.js-pipeline-url-link').click + end + end + end +end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb new file mode 100644 index 00000000000..0835173f1cd --- /dev/null +++ b/qa/qa/page/project/pipeline/show.rb @@ -0,0 +1,35 @@ +module QA::Page + module Project::Pipeline + class Show < QA::Page::Base + view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do + element :pipeline_header, /header class.*ci-header-container.*/ + end + + view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do + element :pipeline_graph, /class.*pipeline-graph.*/ + end + + view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do + element :job_component, /class.*ci-job-component.*/ + end + + view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do + element :status_icon, 'ci-status-icon-${status}' + end + + def running? + within('.ci-header-container') do + return page.has_content?('running') + end + end + + def has_build?(name, status: :success) + within('.pipeline-graph') do + within('.ci-job-component', text: name) do + return has_selector?(".ci-status-icon-#{status}") + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb new file mode 100644 index 00000000000..5270dde7411 --- /dev/null +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -0,0 +1,21 @@ +module QA + module Page + module Project + module Settings + class CICD < Page::Base + include Common + + view 'app/views/projects/settings/ci_cd/show.html.haml' do + element :runners_settings, 'Runners settings' + end + + def expand_runners_settings(&block) + expand_section('Runners settings') do + Settings::Runners.perform(&block) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index b4ef07e1540..1357bf031d5 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -10,6 +10,16 @@ module QA yield end end + + def expand_section(name) + page.within('#content-body') do + page.within('section', text: name) do + click_button 'Expand' + + yield + end + end + end end end end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index f9e40bf4252..332e84724c7 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -14,8 +14,8 @@ module QA end view 'app/assets/javascripts/deploy_keys/components/key.vue' do - element :key_title, /class=".*title.*"/ - element :key_title_field, '{{ deployKey.title }}' + element :key_title, /class=".*qa-key-title.*"/ + element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ end def fill_key_title(title) @@ -31,8 +31,22 @@ module QA end def key_title - page.within('.qa-project-deploy-keys') do - page.find('.title').text + within_project_deploy_keys do + find_element(:key_title).text + end + end + + def key_fingerprint + within_project_deploy_keys do + find_element(:key_fingerprint).text + end + end + + private + + def within_project_deploy_keys + within_element(:project_deploy_keys) do + yield end end end diff --git a/qa/qa/page/project/settings/runners.rb b/qa/qa/page/project/settings/runners.rb new file mode 100644 index 00000000000..b41668c94cd --- /dev/null +++ b/qa/qa/page/project/settings/runners.rb @@ -0,0 +1,35 @@ +module QA + module Page + module Project + module Settings + class Runners < Page::Base + view 'app/views/ci/runner/_how_to_setup_runner.html.haml' do + element :registration_token, '%code#registration_token' + element :coordinator_address, '%code#coordinator_address' + end + + ## + # TODO, phase-out CSS classes added in Ruby helpers. + # + view 'app/helpers/runners_helper.rb' do + # rubocop:disable Lint/InterpolationCheck + element :runner_status, 'runner-status-#{status}' + # rubocop:enable Lint/InterpolationCheck + end + + def registration_token + find('code#registration_token').text + end + + def coordinator_address + find('code#coordinator_address').text + end + + def has_online_runner? + page.has_css?('.runner-status-online') + end + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index c8af5ba6280..5e66e40a0b5 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -33,6 +33,7 @@ module QA def wait_for_push sleep 5 + refresh end end end diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb new file mode 100644 index 00000000000..d456062bce7 --- /dev/null +++ b/qa/qa/runtime/rsa_key.rb @@ -0,0 +1,21 @@ +require 'net/ssh' +require 'forwardable' + +module QA + module Runtime + class RSAKey + extend Forwardable + + attr_reader :key + def_delegators :@key, :fingerprint + + def initialize(bits = 4096) + @key = OpenSSL::PKey::RSA.new(bits) + end + + def public_key + @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}" + end + end + end +end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index 2832439d9e0..60027c89ab1 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -10,17 +10,6 @@ module QA def password ENV['GITLAB_PASSWORD'] || '5iveL!fe' end - - def ssh_key - <<~KEY.delete("\n") - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 - 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 - /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 - M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC - rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 - 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com - KEY - end end end end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb deleted file mode 100644 index ae099fd911e..00000000000 --- a/qa/qa/scenario/entrypoint.rb +++ /dev/null @@ -1,34 +0,0 @@ -module QA - module Scenario - ## - # Base class for running the suite against any GitLab instance, - # including staging and on-premises installation. - # - class Entrypoint < Template - include Bootable - - def perform(address, *files) - Runtime::Scenario.define(:gitlab_address, address) - - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - - Specs::Runner.perform do |specs| - specs.tty = true - specs.tags = self.class.get_tags - specs.files = files.any? ? files : 'qa/specs/features' - end - end - - def self.tags(*tags) - @tags = tags - end - - def self.get_tags - @tags - end - end - end -end diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb new file mode 100644 index 00000000000..b1f24d742e0 --- /dev/null +++ b/qa/qa/scenario/taggable.rb @@ -0,0 +1,17 @@ +module QA + module Scenario + module Taggable + # rubocop:disable Gitlab/ModuleWithInstanceVariables + + def tags(*tags) + @tags = tags + end + + def focus + @tags.to_a + end + + # rubocop:enable Gitlab/ModuleWithInstanceVariables + end + end +end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index e2a1f6bf2bd..993bbd723a3 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -2,11 +2,29 @@ module QA module Scenario module Test ## - # Run test suite against any GitLab instance, + # Base class for running the suite against any GitLab instance, # including staging and on-premises installation. # - class Instance < Entrypoint + class Instance < Template + include Bootable + extend Taggable + tags :core + + def perform(address, *files) + Runtime::Scenario.define(:gitlab_address, address) + + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + + Specs::Runner.perform do |specs| + specs.tty = true + specs.tags = self.class.focus + specs.files = files.any? ? files : 'qa/specs/features' + end + end end end end diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb index 7d0702afdb1..d939f52ab16 100644 --- a/qa/qa/scenario/test/integration/mattermost.rb +++ b/qa/qa/scenario/test/integration/mattermost.rb @@ -6,7 +6,7 @@ module QA # Run test suite against any GitLab instance where mattermost is enabled, # including staging and on-premises installation. # - class Mattermost < Scenario::Entrypoint + class Mattermost < Test::Instance tags :core, :mattermost def perform(address, mattermost, *files) diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb new file mode 100644 index 00000000000..b5c06874e5c --- /dev/null +++ b/qa/qa/service/omnibus.rb @@ -0,0 +1,20 @@ +module QA + module Service + class Omnibus + include Scenario::Actable + include Service::Shellout + + def initialize(container) + @name = container + end + + def gitlab_ctl(command, input: nil) + if input.nil? + shell "docker exec #{@name} gitlab-ctl #{command}" + else + shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" + end + end + end + end +end diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb new file mode 100644 index 00000000000..d0ee33c69f2 --- /dev/null +++ b/qa/qa/service/runner.rb @@ -0,0 +1,41 @@ +require 'securerandom' + +module QA + module Service + class Runner + include Scenario::Actable + include Service::Shellout + + attr_accessor :token, :address, :tags, :image + + def initialize(name) + @image = 'gitlab/gitlab-runner:alpine' + @name = name || "qa-runner-#{SecureRandom.hex(4)}" + @network = Runtime::Scenario.attributes[:network] || 'test' + @tags = %w[qa test] + end + + def pull + shell "docker pull #{@image}" + end + + def register! + shell <<~CMD.tr("\n", ' ') + docker run -d --rm --entrypoint=/bin/sh + --network #{@network} --name #{@name} + -e CI_SERVER_URL=#{@address} + -e REGISTER_NON_INTERACTIVE=true + -e REGISTRATION_TOKEN=#{@token} + -e RUNNER_EXECUTOR=shell + -e RUNNER_TAG_LIST=#{@tags.join(',')} + -e RUNNER_NAME=#{@name} + #{@image} -c 'gitlab-runner register && gitlab-runner run' + CMD + end + + def remove! + shell "docker rm -f #{@name}" + end + end + end +end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb new file mode 100644 index 00000000000..898febde63c --- /dev/null +++ b/qa/qa/service/shellout.rb @@ -0,0 +1,23 @@ +require 'open3' + +module QA + module Service + module Shellout + ## + # TODO, make it possible to use generic QA framework classes + # as a library - gitlab-org/gitlab-qa#94 + # + def shell(command) + puts "Executing `#{command}`" + + Open3.popen2e(command) do |_in, out, wait| + out.each { |line| puts line } + + if wait.value.exited? && wait.value.exitstatus.nonzero? + raise "Command `#{command}` failed!" + end + end + end + end + end +end diff --git a/qa/qa/shell/omnibus.rb b/qa/qa/shell/omnibus.rb deleted file mode 100644 index 6b3628d3109..00000000000 --- a/qa/qa/shell/omnibus.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'open3' - -module QA - module Shell - class Omnibus - include Scenario::Actable - - def initialize(container) - @name = container - end - - def gitlab_ctl(command, input: nil) - if input.nil? - shell "docker exec #{@name} gitlab-ctl #{command}" - else - shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" - end - end - - private - - ## - # TODO, make it possible to use generic QA framework classes - # as a library - gitlab-org/gitlab-qa#94 - # - def shell(command) - puts "Executing `#{command}`" - - Open3.popen2e(command) do |_in, out, wait| - out.each { |line| puts line } - - if wait.value.exited? && wait.value.exitstatus.nonzero? - raise "Docker command `#{command}` failed!" - end - end - end - end - end -end diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb index 7a123e539e1..b9998dda895 100644 --- a/qa/qa/specs/features/project/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb @@ -1,18 +1,20 @@ module QA feature 'deploy keys support', :core do - given(:deploy_key_title) { 'deploy key title' } - given(:deploy_key_value) { Runtime::User.ssh_key } - scenario 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } + key = Runtime::RSAKey.new + deploy_key_title = 'deploy key title' + deploy_key_value = key.public_key + deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| resource.title = deploy_key_title resource.key = deploy_key_value end expect(deploy_key.title).to eq(deploy_key_title) + expect(deploy_key.fingerprint).to eq(key.fingerprint) end end end diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb new file mode 100644 index 00000000000..1bb7730e06c --- /dev/null +++ b/qa/qa/specs/features/project/pipelines_spec.rb @@ -0,0 +1,102 @@ +module QA + feature 'CI/CD Pipelines', :core, :docker do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + + after do + Service::Runner.new(executor).remove! + end + + scenario 'user registers a new specific runner' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Runner.fabricate! do |runner| + runner.name = executor + end + + Page::Project::Settings::CICD.perform do |settings| + sleep 5 # Runner should register within 5 seconds + settings.refresh + + settings.expand_runners_settings do |page| + expect(page).to have_content(executor) + expect(page).to have_online_runner + end + end + end + + scenario 'users creates a new pipeline' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-pipelines' + project.description = 'Project with CI/CD Pipelines.' + end + + Factory::Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = %w[qa test] + end + + Factory::Repository::Push.fabricate! do |push| + push.project = project + push.file_name = '.gitlab-ci.yml' + push.commit_message = 'Add .gitlab-ci.yml' + push.file_content = <<~EOF + test-success: + tags: + - qa + - test + script: echo 'OK' + + test-failure: + tags: + - qa + - test + script: + - echo 'FAILURE' + - exit 1 + + test-tags: + tags: + - qa + - docker + script: echo 'NOOP' + + test-artifacts: + tags: + - qa + - test + script: echo "CONTENTS" > my-artifacts/artifact.txt + artifacts: + paths: + - my-artifacts/ + EOF + end + + Page::Project::Show.act { wait_for_push } + + expect(page).to have_content('Add .gitlab-ci.yml') + + Page::Menu::Side.act { click_ci_cd_pipelines } + + expect(page).to have_content('All 1') + expect(page).to have_content('Add .gitlab-ci.yml') + + puts 'Waiting for the runner to process the pipeline' + sleep 15 # Runner should process all jobs within 15 seconds. + + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to be_running + expect(pipeline).to have_build('test-success', status: :success) + expect(pipeline).to have_build('test-failure', status: :failed) + expect(pipeline).to have_build('test-tags', status: :pending) + expect(pipeline).to have_build('test-artifacts', status: :failed) + end + end + end +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 4f6ffe14c9f..51d9c2c7fd2 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -11,10 +11,7 @@ module QA push.commit_message = 'Add README.md' end - Page::Project::Show.act do - wait_for_push - refresh - end + Page::Project::Show.act { wait_for_push } expect(page).to have_content('README.md') expect(page).to have_content('This is a test project') diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index 90dd58e20fd..c5663049be8 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -19,7 +19,6 @@ describe QA::Factory::Base do it 'returns fabrication product' do allow(subject).to receive(:new).and_return(factory) - allow(factory).to receive(:fabricate!).and_return('something') result = subject.fabricate!('something') diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb new file mode 100644 index 00000000000..ff277b9077b --- /dev/null +++ b/qa/spec/runtime/rsa_key.rb @@ -0,0 +1,9 @@ +describe QA::Runtime::RSAKey do + describe '#public_key' do + subject { described_class.new.public_key } + + it 'generates a public RSA key' do + expect(subject).to match(/\Assh\-rsa AAAA[0-9A-Za-z+\/]+={0,3}\z/) + end + end +end diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/test/instance_spec.rb index aec79dcea04..1824db54c9b 100644 --- a/qa/spec/scenario/entrypoint_spec.rb +++ b/qa/spec/scenario/test/instance_spec.rb @@ -1,6 +1,6 @@ -describe QA::Scenario::Entrypoint do +describe QA::Scenario::Test::Instance do subject do - Class.new(QA::Scenario::Entrypoint) do + Class.new(described_class) do tags :rspec end end diff --git a/spec/fixtures/emails/attachment.eml b/spec/fixtures/emails/attachment.eml index f25c3d1a449..b3a30b3221b 100644 --- a/spec/fixtures/emails/attachment.eml +++ b/spec/fixtures/emails/attachment.eml @@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= + <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= 532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= ax-width:694px" width=3D"45" height=3D"45"> </td> @@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p> <div style=3D"color:#666"> <p>To respond, reply to this email or visit <a href=3D"http://meta.disc= ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co= -lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= +lor:#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back= -up-in-suggested-topics/11005/5</a> in your browser.</p> </div> @@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= lpadding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= + <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= 532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= ax-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size= + <a href=3D"https://meta.discourse.org/users/neil" style=3D"font-size= :13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;c= olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<= /a><br> @@ -155,12 +155,12 @@ vember 19</span> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= + <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= 72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= "max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si= ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ing</a><br> @@ -173,7 +173,7 @@ vember 19</span> <td style=3D"padding-top:5px" colspan=3D"2"> <p style=3D"margin-top:0"><u></u></p><div> <div></div> -<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62= +<img width=3D"20" height=3D"20" src=3D"https://www.gravatar.com/avatar/51d62= 3f33f8b83095db84ff35e15dbe8.png?s=3D40&r=3Dpg&d=3Didenticon" style= =3D"max-width:694px">codinghorror:</div> <blockquote><p style=3D"margin-top:0">I can't even find that topic by n= @@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p> adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= + <img src=3D"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= 5e15dbe8.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"codinghorror" st= yle=3D"max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f= + <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"f= ont-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans= -serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan= k">codinghorror</a><br> @@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can't even find that topic by= adding=3D"0" border=3D"0"><tbody> <tr> <td style=3D"vertical-align:top;width:55px"> - <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= + <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= 72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= "max-width:694px" width=3D"45" height=3D"45"> </td> <td> - <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si= ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= ;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= ing</a><br> @@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in= <p style=3D"margin-top:0"></p> <div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c= -b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m= +b249e.png" target=3D"_blank"><img src=3D"https://cdn.discourse.org/uploads/m= eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig= ht=3D"134" style=3D"max-width:694px"><div> @@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div> <div style=3D"color:#666"> <p>To respond, reply to this email or visit <a href=3D"http://meta.discours= e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:= -#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-= +#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back-up-= in-suggested-topics/11005/5</a> in your browser.</p> </div> <div style=3D"color:#666"> -<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc= +<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.disc= ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre= ferences</a>.</p> </div> diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 5c5d53877a6..da0343588ef 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -117,7 +117,7 @@ describe ApplicationHelper do stub_config_setting(https: false) expect(helper.gravatar_icon(user_email)) - .to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') + .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') end it 'uses HTTPs when configured' do diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb index a11824d0ac5..838ca9fabef 100644 --- a/spec/initializers/settings_spec.rb +++ b/spec/initializers/settings_spec.rb @@ -24,7 +24,7 @@ describe Settings do expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com' expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com' - expect(described_class.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + expect(described_class.host_without_www('https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com' expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com' diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 0e141adb628..7a34126eef7 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -68,7 +68,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit: { @@ -84,7 +84,7 @@ describe('Environment item', () => { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 1339ee00870..68a150f602a 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -14,7 +14,7 @@ "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", "web_url": "http://localhost:3000/u/root" }, "name": "test", diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js index a9783ea065c..323fee3767e 100644 --- a/spec/javascripts/helpers/user_mock_data_helper.js +++ b/spec/javascripts/helpers/user_mock_data_helper.js @@ -4,7 +4,7 @@ export default { for (let i = 0; i < numberUsers; i = i += 1) { users.push( { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: (i + 1), name: `GitLab User ${i}`, username: `gitlab${i}`, diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 43532275121..43589d54be4 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -37,7 +37,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, erase_path: '/root/ci-mock/-/jobs/4757/erase', @@ -54,7 +54,7 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, active: false, @@ -107,10 +107,10 @@ export default { username: 'root', id: 1, state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, - author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + author_gravatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', }, diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index b020a1020df..f0c800c759d 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -107,7 +107,7 @@ export const note = { "name": "Administrator", "username": "root", "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "path": "/root" }, "created_at": "2017-08-10T15:24:03.087Z", diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js new file mode 100644 index 00000000000..3cd33a3e900 --- /dev/null +++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue'; +import eventHub from '~/pages/milestones/shared/event_hub'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('delete_milestone_modal.vue', () => { + const Component = Vue.extend(deleteMilestoneModal); + const props = { + issueCount: 1, + mergeRequestCount: 2, + milestoneId: 3, + milestoneTitle: 'my milestone title', + milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('onSubmit', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + spyOn(eventHub, '$emit'); + }); + + it('deletes milestone and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true }); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if deleting milestone failed', (done) => { + const dummyError = new Error('deleting milestone failed'); + dummyError.response = { status: 418 }; + spyOn(axios, 'delete').and.callFake((url) => { + expect(url).toBe(props.milestoneUrl); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl); + eventHub.$emit.calls.reset(); + return Promise.reject(dummyError); + }); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + + vm.onSubmit() + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false }); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('text', () => { + it('contains the issue and milestone count', () => { + vm = mountComponent(Component, props); + const value = vm.text; + + expect(value).toContain('remove it from 1 issue and 2 merge requests'); + }); + + it('contains neither issue nor milestone count', () => { + vm = mountComponent(Component, { ...props, + issueCount: 0, + mergeRequestCount: 0, + }); + + const value = vm.text; + + expect(value).toContain('is not currently used'); + }); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 7bc591d2d47..d9e84e35f69 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -27,7 +27,7 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/user0', }, { @@ -35,7 +35,7 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/tajuana', }, { @@ -43,7 +43,7 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/michaele.will', }, ], @@ -72,24 +72,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], human_time_estimate: null, @@ -100,24 +100,24 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/user0', + avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/user0', }, { name: 'Marguerite Bartell', username: 'tajuana', id: 18, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/tajuana', + avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/tajuana', }, { name: 'Laureen Ritchie', username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', - web_url: 'http: //localhost:3001/michaele.will', + avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + web_url: 'http://localhost:3001/michaele.will', }, ], subscribed: true, @@ -182,7 +182,7 @@ const mockData = { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, rootPath: '/', fullPath: '/gitlab-org/gitlab-shell', @@ -194,7 +194,7 @@ const mockData = { human_total_time_spent: null, }, user: { - avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', id: 1, name: 'Administrator', username: 'root', diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index ea4eae1e23f..3591f96ff87 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -6,14 +6,14 @@ const ASSIGNEE = { id: 2, name: 'gitlab user 2', username: 'gitlab2', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const ANOTHER_ASSINEE = { id: 3, name: 'gitlab user 3', username: 'gitlab3', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }; const PARTICIPANT = { @@ -38,7 +38,7 @@ describe('Sidebar store', () => { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, editable: true, rootPath: '/', diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js new file mode 100644 index 00000000000..c39fcda0071 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('MR widget status icon component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(mrStatusIcon); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('while loading', () => { + it('renders loading icon', () => { + vm = mountComponent(Component, { status: 'loading' }); + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + }); + + describe('with status icon', () => { + it('renders ci status icon', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull(); + }); + }); + + describe('with disabled button', () => { + it('renders a disabled button', () => { + vm = mountComponent(Component, { status: 'failed', showDisabledButton: true }); + expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge'); + }); + }); + + describe('without disabled button', () => { + it('does not render a disabled button', () => { + vm = mountComponent(Component, { status: 'failed' }); + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 6b7aa935ad3..658cadddb81 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -1,19 +1,29 @@ import Vue from 'vue'; -import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking'; +import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetChecking', () => { - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(checkingComponent); - const el = new Component({ - el: document.createElement('div'), - }).$el; + let Component; + let vm; - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); - expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('Checking ability to merge automatically'); - expect(el.querySelector('i')).toBeDefined(); - }); + beforeEach(() => { + Component = Vue.extend(checkingComponent); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders disabled button', () => { + expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + }); + + it('renders information about merging', () => { + expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 1bf97bbf093..51a34739ee9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -1,74 +1,58 @@ import Vue from 'vue'; -import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed'; - -const mr = { - targetBranch: 'good-branch', - targetBranchPath: '/good-branch', - metrics: { - mergedBy: {}, - mergedAt: 'mergedUpdatedAt', - closedBy: { - name: 'Fatih Acet', - username: 'fatihacet', - }, - closedAt: 'closedEventUpdatedAt', - readableMergedAt: '', - readableClosedAt: '', - }, - updatedAt: 'mrUpdatedAt', - closedAt: '1 day ago', -}; - -const createComponent = () => { - const Component = Vue.extend(closedComponent); - - return new Component({ - el: document.createElement('div'), - propsData: { mr }, - }); -}; +import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetClosed', () => { - describe('props', () => { - it('should have props', () => { - const mrProp = closedComponent.props.mr; - - expect(mrProp.type instanceof Object).toBeTruthy(); - expect(mrProp.required).toBeTruthy(); - }); + let vm; + + beforeEach(() => { + const Component = Vue.extend(closedComponent); + vm = mountComponent(Component, { mr: { + metrics: { + mergedBy: {}, + closedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm GMT+0000', + closedAt: 'Jan 24, 2018 1:02pm GMT+0000', + readableMergedAt: '', + readableClosedAt: 'less than a minute ago', + }, + targetBranchPath: '/twitter/flight/commits/so_long_jquery', + targetBranch: 'so_long_jquery', + } }); }); - describe('components', () => { - it('should have components added', () => { - expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined(); - }); + afterEach(() => { + vm.$destroy(); }); - describe('template', () => { - let vm; - let el; + it('renders warning icon', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + }); - beforeEach(() => { - vm = createComponent(); - el = vm.$el; - }); + it('renders closed by information with author and time', () => { + expect( + vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain( + 'Closed by Administrator less than a minute ago', + ); + }); - afterEach(() => { - vm.$destroy(); - }); + it('links to the user that closed the MR', () => { + expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root'); + }); - it('should have correct elements', () => { - expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name); - expect(el.textContent).toContain('The changes were not merged into'); - expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); - }); + it('renders information about the changes not being merged', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '), + ).toContain('The changes were not merged into so_long_jquery'); + }); - it('should use closedEvent updatedAt as tooltip title', () => { - expect( - el.querySelector('time').getAttribute('title'), - ).toBe('closedEventUpdatedAt'); - }); + it('renders link for target branch', () => { + expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index 5d4c7ec09dc..a7d69fdcdb9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,105 +1,85 @@ import Vue from 'vue'; -import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts'; +import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import mountComponent from '../../../helpers/vue_mount_component_helper'; -const ConflictsComponent = Vue.extend(conflictsComponent); -const path = '/conflicts'; - describe('MRWidgetConflicts', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = conflictsComponent.props; + let Component; + let vm; + const path = '/conflicts'; - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); + beforeEach(() => { + Component = Vue.extend(conflictsComponent); }); - describe('template', () => { - describe('when allowed to merge', () => { - let vm; - - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: true, - conflictResolutionPath: path, - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('should tell you about conflicts without bothering other people', () => { - expect(vm.$el.textContent).toContain('There are merge conflicts'); - expect(vm.$el.textContent).not.toContain('ask someone with write access'); - }); - - it('should allow you to resolve the conflicts', () => { - const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); + afterEach(() => { + vm.$destroy(); + }); - expect(resolveButton.textContent).toContain('Resolve conflicts'); - expect(resolveButton.getAttribute('href')).toEqual(path); + describe('when allowed to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: true, + conflictResolutionPath: path, + }, }); + }); - it('should have merge buttons', () => { - const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); - const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - - expect(mergeButton.textContent).toContain('Merge'); - expect(mergeButton.disabled).toBeTruthy(); - expect(mergeButton.classList.contains('btn-success')).toEqual(true); - expect(mergeLocallyButton.textContent).toContain('Merge locally'); - }); + it('should tell you about conflicts without bothering other people', () => { + expect(vm.$el.textContent).toContain('There are merge conflicts'); + expect(vm.$el.textContent).not.toContain('ask someone with write access'); }); - describe('when user does not have permission to merge', () => { - let vm; + it('should allow you to resolve the conflicts', () => { + const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - canMerge: false, - }, - }); - }); + expect(resolveButton.textContent).toContain('Resolve conflicts'); + expect(resolveButton.getAttribute('href')).toEqual(path); + }); - afterEach(() => { - vm.$destroy(); - }); + it('should have merge buttons', () => { + const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); + const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button'); - it('should show proper message', () => { - expect(vm.$el.textContent).toContain('ask someone with write access'); - }); + expect(mergeButton.textContent).toContain('Merge'); + expect(mergeButton.disabled).toBeTruthy(); + expect(mergeButton.classList.contains('btn-success')).toEqual(true); + expect(mergeLocallyButton.textContent).toContain('Merge locally'); + }); + }); - it('should not have action buttons', () => { - expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); - expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); - expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + describe('when user does not have permission to merge', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + canMerge: false, + }, }); }); - describe('when fast-forward or semi-linear merge enabled', () => { - let vm; + it('should show proper message', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access'); + }); - beforeEach(() => { - vm = mountComponent(ConflictsComponent, { - mr: { - shouldBeRebased: true, - }, - }); - }); + it('should not have action buttons', () => { + expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); + expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); + }); + }); - afterEach(() => { - vm.$destroy(); + describe('when fast-forward or semi-linear merge enabled', () => { + beforeEach(() => { + vm = mountComponent(Component, { + mr: { + shouldBeRebased: true, + }, }); + }); - it('should tell you to rebase locally', () => { - expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.'); - expect(vm.$el.textContent).toContain('To merge this request, first rebase locally'); - }); + it('should tell you to rebase locally', () => { + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js deleted file mode 100644 index 237035648cf..00000000000 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging'; - -describe('MRWidgetMerging', () => { - describe('props', () => { - it('should have props', () => { - const { mr } = mergingComponent.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - }); - }); - - describe('template', () => { - it('should have correct elements', () => { - const Component = Vue.extend(mergingComponent); - const mr = { - targetBranchPath: '/branch-path', - targetBranch: 'branch', - }; - const el = new Component({ - el: document.createElement('div'), - propsData: { mr }, - }).$el; - - expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('This merge request is in the process of being merged'); - expect(el.innerText).toContain('changes will be merged into'); - expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath); - expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js new file mode 100644 index 00000000000..0b2ed2d4086 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('MRWidgetMerging', () => { + let vm; + beforeEach(() => { + const Component = Vue.extend(mergingComponent); + + vm = mountComponent(Component, { mr: { + targetBranchPath: '/branch-path', + targetBranch: 'branch', + } }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders information about merge request being merged', () => { + expect( + vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toContain('This merge request is in the process of being merged'); + }); + + it('renders branch information', () => { + expect( + vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '), + ).toEqual('The changes will be merged into branch'); + expect( + vm.$el.querySelector('a').getAttribute('href'), + ).toEqual('/branch-path'); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index ae494267659..3dd75307484 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -38,7 +38,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "merged_at": "2017-04-07T15:39:25.696Z", @@ -50,7 +50,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "merge_user": null, @@ -64,7 +64,7 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, "active": false, @@ -159,10 +159,10 @@ export default { "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, - "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" }, diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb new file mode 100644 index 00000000000..5d22dcfb508 --- /dev/null +++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + + subject { described_class.new(repository, 'lfs') } + + it 'loads .gitattributes blob' do + repository.raw # Initialize repository in advance since this also checks attributes + + expected_filter = 'filter=lfs diff=lfs merge=lfs' + receive_blob = receive(:new).with(a_string_including(expected_filter)) + expect(Gitlab::Git::AttributesParser).to receive_blob.and_call_original + + subject + end + + it 'handles missing blobs' do + expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error + end + + describe '#attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.lfs')['filter']).to eq('lfs') + end + end +end diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index b715fc3410a..323334e99a5 100644 --- a/spec/lib/gitlab/git/attributes_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -describe Gitlab::Git::Attributes, seed_helper: true do - let(:path) do - File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') - end +describe Gitlab::Git::AttributesParser, seed_helper: true do + let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') } + let(:data) { File.read(attributes_path) } - subject { described_class.new(path) } + subject { described_class.new(data) } describe '#attributes' do context 'using a path with attributes' do @@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do expect(subject.attributes('test.foo')).to eq({}) end end + + context 'when attributes data is a file handle' do + subject do + File.open(attributes_path, 'r') do |file_handle| + described_class.new(file_handle) + end + end + + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + end + + context 'when attributes data is nil' do + let(:data) { nil } + + it 'returns an empty Hash' do + expect(subject.attributes('test.foo')).to eq({}) + end + end end describe '#patterns' do @@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'parses an entry that uses a tab to separate the pattern and attributes' do - expect(subject.patterns[File.join(path, '*.md')]) + expect(subject.patterns[File.join('/', '*.md')]) .to eq({ 'gitlab-language' => 'markdown' }) end it 'stores patterns in reverse order' do first = subject.patterns.to_a[0] - expect(first[0]).to eq(File.join(path, 'bla/bla.txt')) + expect(first[0]).to eq(File.join('/', 'bla/bla.txt')) end # It's a bit hard to test for something _not_ being processed. As such we'll @@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'ignores any comments and empty lines' do expect(subject.patterns.length).to eq(10) end - - it 'does not parse anything when the attributes file does not exist' do - expect(File).to receive(:exist?) - .with(File.join(path, 'info/attributes')) - .and_return(false) - - expect(subject.patterns).to eq({}) - end end describe '#parse_attributes' do @@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) end - it 'does not yield when the attributes file does not exist' do - expect(File).to receive(:exist?) - .with(File.join(path, 'info/attributes')) - .and_return(false) - - expect { |b| subject.each_line(&b) }.not_to yield_control - end - it 'does not yield when the attributes file has an unsupported encoding' do - path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') - attrs = described_class.new(path) + path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes') + attrs = described_class.new(File.read(path)) expect { |b| attrs.each_line(&b) }.not_to yield_control end diff --git a/spec/lib/gitlab/git/info_attributes_spec.rb b/spec/lib/gitlab/git/info_attributes_spec.rb new file mode 100644 index 00000000000..ea84909c3e0 --- /dev/null +++ b/spec/lib/gitlab/git/info_attributes_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Git::InfoAttributes, seed_helper: true do + let(:path) do + File.join(SEED_STORAGE_PATH, 'with-git-attributes.git') + end + + subject { described_class.new(path) } + + describe '#attributes' do + context 'using a path with attributes' do + it 'returns the attributes as a Hash' do + expect(subject.attributes('test.txt')).to eq({ 'text' => true }) + end + + it 'returns an empty Hash for a defined path without attributes' do + expect(subject.attributes('bla/bla.txt')).to eq({}) + end + end + end + + describe '#parser' do + it 'parses a file with entries' do + expect(subject.patterns).to be_an_instance_of(Hash) + expect(subject.patterns["/*.txt"]).to eq({ 'text' => true }) + end + + it 'does not parse anything when the attributes file does not exist' do + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) + + expect(subject.patterns).to eq({}) + end + + it 'does not parse attributes files with unsupported encoding' do + path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git') + subject = described_class.new(path) + + expect(subject.patterns).to eq({}) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 36ca3980de9..18b2e55e659 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -899,6 +899,44 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + context "compare results between log_by_walk and log_by_shell" do + let(:options) { { ref: "master" } } + let(:commits_by_walk) { repository.log(options).map(&:id) } + let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with limit" do + let(:options) { { ref: "master", limit: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with offset" do + let(:options) { { ref: "master", offset: 1 } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with skip_merges" do + let(:options) { { ref: "master", skip_merges: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + + context "with path" do + let(:options) { { ref: "master", path: "encoding" } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + + context "with follow" do + let(:options) { { ref: "master", path: "encoding", follow: true } } + + it { expect(commits_by_walk).to eq(commits_by_shell) } + end + end + end + context "where provides 'after' timestamp" do options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index b2275119a04..3722a91c050 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -131,6 +131,29 @@ describe Gitlab::GitalyClient::CommitService do end end + describe '#commit_count' do + before do + expect_any_instance_of(Gitaly::CommitService::Stub) + .to receive(:count_commits) + .with(gitaly_request_with_path(storage_name, relative_path), + kind_of(Hash)) + .and_return([]) + end + + it 'sends a commit_count message' do + client.commit_count(revision) + end + + context 'with UTF-8 params strings' do + let(:revision) { "branch\u011F" } + let(:path) { "foo/\u011F.txt" } + + it 'handles string encodings correctly' do + client.commit_count(revision, path: path) + end + end + end + describe '#find_commit' do let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' } it 'sends an RPC request' do diff --git a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb new file mode 100644 index 00000000000..2c7e5eb5787 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::HealthCheckService do + let(:project) { create(:project) } + let(:storage_name) { project.repository_storage } + + subject { described_class.new(storage_name) } + + describe '#check' do + it 'successfully sends a health check request' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout).and_call_original + + expect(subject.check).to eq({ success: true }) + end + + it 'receives an unsuccessful health check request' do + expect_any_instance_of(Grpc::Health::V1::Health::Stub) + .to receive(:check) + .and_return(double(status: false)) + + expect(subject.check).to eq({ success: false }) + end + + it 'gracefully handles gRPC error' do + expect(Gitlab::GitalyClient).to receive(:call).with( + storage_name, + :health_check, + :check, + instance_of(Grpc::Health::V1::HealthCheckRequest), + timeout: Gitlab::GitalyClient.fast_timeout) + .and_raise(GRPC::Unavailable.new('Connection refused')) + + expect(subject.check).to eq({ success: false, message: '14:Connection refused' }) + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 309b7338ef0..81bcd8c28ed 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -3,6 +3,31 @@ require 'spec_helper' # We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want # those stubs while testing the GitalyClient itself. describe Gitlab::GitalyClient, skip_gitaly_mock: true do + describe '.stub_class' do + it 'returns the gRPC health check stub' do + expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) + end + + it 'returns a Gitaly stub' do + expect(described_class.stub_class(:ref_service)).to eq(::Gitaly::RefService::Stub) + end + end + + describe '.stub_address' do + it 'returns the same result after being called multiple times' do + address = 'localhost:9876' + prefixed_address = "tcp://#{address}" + + allow(Gitlab.config.repositories).to receive(:storages).and_return({ + 'default' => { 'gitaly_address' => prefixed_address } + }) + + 2.times do + expect(described_class.stub_address('default')).to eq('localhost:9876') + end + end + end + describe '.stub' do # Notice that this is referring to gRPC "stubs", not rspec stubs before do diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb new file mode 100644 index 00000000000..724beefff69 --- /dev/null +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::HealthChecks::GitalyCheck do + let(:result_class) { Gitlab::HealthChecks::Result } + let(:repository_storages) { ['default'] } + + before do + allow(described_class).to receive(:repository_storages) { repository_storages } + end + + describe '#readiness' do + subject { described_class.readiness } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) } + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) } + end + end + + describe '#metrics' do + subject { described_class.metrics } + + before do + expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check) + end + + context 'Gitaly server is up' do + let(:gitaly_check) { double(check: { success: true }) } + + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: 'default' })) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 1)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + + context 'Gitaly server is down' do + let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) } + + it 'provides metrics' do + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0)) + expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0)) + end + end + end +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index ea75434e399..cc9d79da708 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -386,6 +386,17 @@ describe WikiPage do end end + describe '#formatted_content' do + it 'returns processed content of the page', :disable_gitaly do + subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" }) + page = wiki.find_page('RDoc') + + expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n") + + destroy_page('RDoc') + end + end + private def remove_temp_repo(path) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index fd6368e7b40..9e5f08fbc51 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -20,7 +20,7 @@ module TestEnv 'improve/awesome' => '5937ac0', 'merged-target' => '21751bf', 'markdown' => '0ed8c6c', - 'lfs' => 'be93687', + 'lfs' => '55bc176', 'master' => 'b83d6e3', 'merge-test' => '5937ac0', "'test'" => 'e56497b', diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb index 95f0be49412..7b300150874 100644 --- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb +++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb @@ -13,8 +13,8 @@ describe 'projects/pipelines_settings/_show' do render expect(rendered).to have_css('.settings-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the') - expect(rendered).to have_link('Kubernetes service') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a') + expect(rendered).to have_link('Kubernetes cluster') end end @@ -27,8 +27,8 @@ describe 'projects/pipelines_settings/_show' do render expect(rendered).to have_css('.settings-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the') - expect(rendered).to have_link('Kubernetes service') + expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a') + expect(rendered).to have_link('Kubernetes cluster') end end end diff --git a/yarn.lock b/yarn.lock index e66affe2bd4..d10a4372a40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.6.0.tgz#08fa5f2e80b7ac4e4713f71fe8a684bd34430c9d" +"@gitlab-org/gitlab-svgs@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.7.0.tgz#dbb1330a1b1ee478378dddab53fe1a881e810f5d" "@types/jquery@^2.0.40": version "2.0.48" |