diff options
123 files changed, 1432 insertions, 415 deletions
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.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index 04e1766b8c7..caeaac75b45 100644 --- 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 @@ -1,5 +1,5 @@ <script> - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetChecking', 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 index 8c0ce43c76c..71bfdaf801e 100644 --- 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 @@ -1,6 +1,6 @@ <script> import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetClosed', 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 index 13b07f82330..dad4b0fe49d 100644 --- 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 @@ -1,5 +1,5 @@ <script> - import statusIcon from '../mr_widget_status_icon'; + import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetConflicts', 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 5e8e251428a..8651945a3da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li 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.vue'; -export { default as MergingState } from './components/states/mr_widget_merging'; +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.vue'; 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/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/models/merge_request.rb b/app/models/merge_request.rb index 8028ff3875b..4accb08eaf9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -989,8 +989,14 @@ class MergeRequest < ActiveRecord::Base merged_at = metrics&.merged_at notes_association = notes_with_associations + # It is not guaranteed that Note#created_at will be strictly later than + # MergeRequestMetric#merged_at. Nanoseconds on MySQL may break this + # comparison, as will a HA environment if clocks are not *precisely* + # synchronized. Add a minute's leeway to compensate for both possibilities + cutoff = merged_at - 1.minute + if merged_at - notes_association = notes_association.where('created_at > ?', merged_at) + notes_association = notes_association.where('created_at >= ?', cutoff) end !merge_commit.has_been_reverted?(current_user, notes_association) 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/_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/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/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/gitaly-repo-exists.yml b/changelogs/unreleased/gitaly-repo-exists.yml new file mode 100644 index 00000000000..a9eb42a2038 --- /dev/null +++ b/changelogs/unreleased/gitaly-repo-exists.yml @@ -0,0 +1,5 @@ +--- +title: Make Gitaly RepositoryExists opt-out +merge_request: 16680 +author: +type: other 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/ux-guide-deprecation.yml b/changelogs/unreleased/ux-guide-deprecation.yml new file mode 100644 index 00000000000..16477f59abf --- /dev/null +++ b/changelogs/unreleased/ux-guide-deprecation.yml @@ -0,0 +1,6 @@ +--- +title: Add note within ux documentation that further changes should be made within + the design.gitlab project +merge_request: +author: +type: deprecated 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/development/ux_guide/index.md b/doc/development/ux_guide/index.md index 42bcf234e12..c59e7b72a1a 100644 --- a/doc/development/ux_guide/index.md +++ b/doc/development/ux_guide/index.md @@ -1,3 +1,5 @@ +> We are in the process of transferring UX documentation to the [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com) project. Any updates to these docs should be made in that project. If documentation does not yet exist within [design.gitlab.com](https://gitlab.com/gitlab-org/design.gitlab.com), [create an issue](https://gitlab.com/gitlab-org/design.gitlab.com/issues) and merge request to add your new changes. + # GitLab UX Guide The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions. 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..6ecb3ad6c70 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) @@ -133,7 +133,7 @@ module Gitlab end def exists? - Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| + Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| if enabled gitaly_repository_client.exists? else @@ -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/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_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/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c76f32b3989..429b6615131 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1127,9 +1127,19 @@ describe MergeRequest do end end - context 'when the revert commit is mentioned in a note before the MR was merged' do + context 'when the revert commit is mentioned in a note just before the MR was merged' do before do - subject.notes.last.update!(created_at: subject.metrics.merged_at - 1.second) + subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds) + end + + it 'returns false' do + expect(subject.can_be_reverted?(current_user)).to be_falsey + end + end + + context 'when the revert commit is mentioned in a note long before the MR was merged' do + before do + subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes) end it 'returns true' do 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/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" |