diff options
295 files changed, 3016 insertions, 546 deletions
@@ -8,7 +8,8 @@ "plugins": [ ["istanbul", { "exclude": [ - "spec/javascripts/**/*" + "spec/javascripts/**/*", + "app/assets/javascripts/locale/**/app.js" ] }], ["transform-define", { diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4f47d3f0171..f038ce72aeb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -604,6 +604,7 @@ codequality: paths: [codeclimate.json] sast: + <<: *except-docs image: registry.gitlab.com/gitlab-org/gl-sast:latest before_script: [] script: @@ -623,6 +624,18 @@ qa:internal: - bundle install - bundle exec rspec +qa:selectors: + <<: *dedicated-runner + <<: *except-docs + stage: test + variables: + SETUP_DB: "false" + services: [] + script: + - cd qa/ + - bundle install + - bundle exec bin/qa Test::Sanity::Selectors + coverage: <<: *dedicated-runner <<: *except-docs-and-qa diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 18091983f59..1545d966571 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -3.4.0 +3.5.0 @@ -385,9 +385,6 @@ gem 'ruby-prof', '~> 0.16.2' # OAuth gem 'oauth2', '~> 1.4' -# Soft deletion -gem 'paranoia', '~> 2.3.1' - # Health check gem 'health_check', '~> 2.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 40c4f73b8a6..cb6b0ebb3bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -580,8 +580,6 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.0) - paranoia (2.3.1) - activerecord (>= 4.0, < 5.2) parser (2.4.0.2) ast (~> 2.3) parslet (1.5.0) @@ -1117,7 +1115,6 @@ DEPENDENCIES omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) - paranoia (~> 2.3.1) peek (~> 1.0.1) peek-gc (~> 0.0.2) peek-host (~> 1.0.0) diff --git a/app/assets/images/multi-editor-on.png b/app/assets/images/multi-editor-on.png Binary files differindex 2bcd29abf13..d51b68da985 100644 --- a/app/assets/images/multi-editor-on.png +++ b/app/assets/images/multi-editor-on.png diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index a282b67b0fc..72a6426e901 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -12,11 +12,6 @@ import notificationsDropdown from './notifications_dropdown'; import groupAvatar from './group_avatar'; import GroupLabelSubscription from './group_label_subscription'; import LineHighlighter from './line_highlighter'; -import BuildArtifacts from './build_artifacts'; -import groupsSelect from './groups_select'; -import Search from './search'; -import initAdmin from './admin'; -import NamespaceSelect from './namespace_select'; import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; @@ -37,35 +32,25 @@ import BindInOut from './behaviors/bind_in_out'; import SecretValues from './behaviors/secret_values'; import DeleteModal from './branches/branches_delete_modal'; import Group from './group'; -import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import setupProjectEdit from './project_edit'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; -import Landing from './landing'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; import ShortcutsWiki from './shortcuts_wiki'; -import Pipelines from './pipelines'; import BlobViewer from './blob/viewer/index'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import UsersSelect from './users_select'; import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; import ShortcutsBlob from './shortcuts_blob'; -import SigninTabsMemoizer from './signin_tabs_memoizer'; import Star from './star'; import TreeView from './tree'; -import UsagePing from './usage_ping'; -import UsernameValidator from './username_validator'; -import VersionCheckImage from './version_check_image'; import Wikis from './wikis'; import ZenMode from './zen_mode'; import initSettingsPanels from './settings_panels'; -import initExperimentalFlags from './experimental_flags'; -import OAuthRememberMe from './oauth_remember_me'; import PerformanceBar from './performance_bar'; -import initBroadcastMessagesForm from './broadcast_message'; import initNotes from './init_notes'; import initLegacyFilters from './init_legacy_filters'; import initIssuableSidebar from './init_issuable_sidebar'; @@ -73,7 +58,6 @@ import initProjectVisibilitySelector from './project_visibility'; import GpgBadges from './gpg_badges'; import initChangesDropdown from './init_changes_dropdown'; import NewGroupChild from './groups/new_group_child'; -import AbuseReports from './abuse_reports'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import AjaxLoadingSpinner from './ajax_loading_spinner'; import GlFieldErrors from './gl_field_errors'; @@ -157,13 +141,10 @@ import Activities from './activities'; const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); switch (page) { - case 'profiles:preferences:show': - initExperimentalFlags(); - break; case 'sessions:new': - new UsernameValidator(); - new SigninTabsMemoizer(); - new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); + import('./pages/sessions/new') + .then(callDefault) + .catch(fail); break; case 'projects:boards:show': case 'projects:boards:index': @@ -232,19 +213,14 @@ import Activities from './activities'; case 'explore:projects:index': case 'explore:projects:trending': case 'explore:projects:starred': - case 'admin:projects:index': - new ProjectsList(); + import('./pages/explore/projects') + .then(callDefault) + .catch(fail); break; case 'explore:groups:index': - new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); - if (!landingElement) break; - const exploreGroupsLanding = new Landing( - landingElement, - landingElement.querySelector('.dismiss-button'), - 'explore_groups_landing_dismissed', - ); - exploreGroupsLanding.toggle(); + import('./pages/explore/groups') + .then(callDefault) + .catch(fail); break; case 'projects:milestones:new': case 'projects:milestones:edit': @@ -349,7 +325,9 @@ import Activities from './activities'; shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': - new Activities(); + import('./pages/dashboard/activity') + .then(callDefault) + .catch(fail); break; case 'projects:commit:show': new Diff(); @@ -370,8 +348,10 @@ import Activities from './activities'; $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:activity': - new Activities(); - shortcut_handler = new ShortcutsNavigation(); + import('./pages/projects/activity') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commits:show': CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); @@ -403,23 +383,16 @@ import Activities from './activities'; break; case 'projects:pipelines:new': case 'projects:pipelines:create': - new NewBranchForm($('.js-new-pipeline-form')); + import('./pages/projects/pipelines/new') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': - const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; - - new Pipelines({ - initTabs: true, - pipelineStatusUrl, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); + import('./pages/projects/pipelines/builds') + .then(callDefault) + .catch(fail); break; case 'groups:activity': new Activities(); @@ -441,22 +414,28 @@ import Activities from './activities'; new UsersSelect(); break; case 'projects:project_members:index': - memberExpirationDate('.js-access-expiration-date-groups'); - groupsSelect(); - memberExpirationDate(); - new Members(); - new UsersSelect(); + import('./pages/projects/project_members/') + .then(callDefault) + .catch(fail); break; case 'groups:new': - case 'admin:groups:new': case 'groups:create': - case 'admin:groups:create': BindInOut.initAll(); new Group(); groupAvatar(); break; - case 'groups:edit': + case 'admin:groups:create': + case 'admin:groups:new': + import('./pages/admin/groups/new') + .then(callDefault) + .catch(fail); + break; case 'admin:groups:edit': + import('./pages/admin/groups/edit') + .then(callDefault) + .catch(fail); + break; + case 'groups:edit': groupAvatar(); break; case 'projects:tree:show': @@ -513,22 +492,30 @@ import Activities from './activities'; break; case 'projects:forks:new': import(/* webpackChunkName: 'project_fork' */ './project_fork') - .then(fork => fork.default()) - .catch(() => {}); + .then(callDefault) + .catch(fail); break; case 'projects:artifacts:browse': - new ShortcutsNavigation(); - new BuildArtifacts(); + import('./pages/projects/artifacts/browse') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:artifacts:file': - new ShortcutsNavigation(); - new BlobViewer(); + import('./pages/projects/artifacts/file') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'help:index': - VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + import('./pages/help') + .then(callDefault) + .catch(fail); break; case 'search:show': - new Search(); + import('./pages/search/show') + .then(callDefault) + .catch(fail); break; case 'projects:settings:repository:show': // Initialize expandable settings panels @@ -567,8 +554,14 @@ import Activities from './activities'; import('./pages/import/fogbugz/new_user_map').then(m => m.default()).catch(fail); break; case 'profiles:personal_access_tokens:index': + import('./pages/profiles/personal_access_tokens') + .then(callDefault) + .catch(fail); + break; case 'admin:impersonation_tokens:index': - new DueDateSelectors(); + import('./pages/admin/impersonation_tokens') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:show': import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') @@ -602,29 +595,51 @@ import Activities from './activities'; // needed in rspec gl.u2fAuthenticate = u2fAuthenticate; case 'admin': - initAdmin(); + import('./pages/admin') + .then(callDefault) + .catch(fail); switch (path[1]) { case 'broadcast_messages': - initBroadcastMessagesForm(); + import('./pages/admin/broadcast_messages') + .then(callDefault) + .catch(fail); break; case 'cohorts': - new UsagePing(); + import('./pages/admin/cohorts') + .then(callDefault) + .catch(fail); break; case 'groups': - new UsersSelect(); + switch (path[2]) { + case 'show': + import('./pages/admin/groups/show') + .then(callDefault) + .catch(fail); + break; + } break; case 'projects': - document.querySelectorAll('.js-namespace-select') - .forEach(dropdown => new NamespaceSelect({ dropdown })); + import('./pages/admin/projects') + .then(callDefault) + .catch(fail); break; case 'labels': switch (path[2]) { case 'new': + import('./pages/admin/labels/new') + .then(callDefault) + .catch(fail); + break; case 'edit': - new Labels(); + import('./pages/admin/labels/edit') + .then(callDefault) + .catch(fail); + break; } case 'abuse_reports': - new AbuseReports(); + import('./pages/admin/abuse_reports') + .then(callDefault) + .catch(fail); break; } break; @@ -633,8 +648,9 @@ import Activities from './activities'; new UserCallout(); break; case 'profiles': - new NotificationsForm(); - notificationsDropdown(); + import('./pages/profiles/index/') + .then(callDefault) + .catch(fail); break; case 'projects': new Project(); @@ -647,8 +663,8 @@ import Activities from './activities'; shortcut_handler = new ShortcutsNavigation(); new ProjectNew(); import(/* webpackChunkName: 'project_permissions' */ './projects/permissions') - .then(permissions => permissions.default()) - .catch(() => {}); + .then(callDefault) + .catch(fail); break; case 'new': new ProjectNew(); diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 400306759b2..e035ba462db 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,16 +1,20 @@ <script> /* global Flash */ +import { s__ } from '~/locale'; +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import modal from '~/vue_shared/components/modal.vue'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; + import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { COMMON_STR } from '../constants'; -import { mergeUrlParams } from '../../lib/utils/url_utility'; import groupsComponent from './groups.vue'; export default { components: { loadingIcon, + modal, groupsComponent, }, props: { @@ -32,6 +36,10 @@ export default { isLoading: true, isSearchEmpty: false, searchEmptyMessage: '', + showModal: false, + groupLeaveConfirmationMessage: '', + targetGroup: null, + targetParentGroup: null, }; }, computed: { @@ -48,7 +56,7 @@ export default { eventHub.$on('fetchPage', this.fetchPage); eventHub.$on('toggleChildren', this.toggleChildren); - eventHub.$on('leaveGroup', this.leaveGroup); + eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal); eventHub.$on('updatePagination', this.updatePagination); eventHub.$on('updateGroups', this.updateGroups); }, @@ -58,7 +66,7 @@ export default { beforeDestroy() { eventHub.$off('fetchPage', this.fetchPage); eventHub.$off('toggleChildren', this.toggleChildren); - eventHub.$off('leaveGroup', this.leaveGroup); + eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal); eventHub.$off('updatePagination', this.updatePagination); eventHub.$off('updateGroups', this.updateGroups); }, @@ -141,14 +149,23 @@ export default { parentGroup.isOpen = false; } }, - leaveGroup(group, parentGroup) { - const targetGroup = group; - targetGroup.isBeingRemoved = true; - this.service.leaveGroup(targetGroup.leavePath) + showLeaveGroupModal(group, parentGroup) { + this.targetGroup = group; + this.targetParentGroup = parentGroup; + this.showModal = true; + this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`); + }, + hideLeaveGroupModal() { + this.showModal = false; + }, + leaveGroup() { + this.showModal = false; + this.targetGroup.isBeingRemoved = true; + this.service.leaveGroup(this.targetGroup.leavePath) .then(res => res.json()) .then((res) => { $.scrollTo(0); - this.store.removeGroup(targetGroup, parentGroup); + this.store.removeGroup(this.targetGroup, this.targetParentGroup); Flash(res.notice, 'notice'); }) .catch((err) => { @@ -157,7 +174,7 @@ export default { message = COMMON_STR.LEAVE_FORBIDDEN; } Flash(message); - targetGroup.isBeingRemoved = false; + this.targetGroup.isBeingRemoved = false; }); }, updatePagination(headers) { @@ -190,5 +207,14 @@ export default { :search-empty-message="searchEmptyMessage" :page-info="pageInfo" /> + <modal + v-show="showModal" + :primary-button-label="__('Leave')" + kind="warning" + :title="__('Are you sure?')" + :text="groupLeaveConfirmationMessage" + @cancel="hideLeaveGroupModal" + @submit="leaveGroup" + /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 1bde6ae5185..87065b3d6e3 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,56 +1,41 @@ <script> - import { s__ } from '~/locale'; - import tooltip from '~/vue_shared/directives/tooltip'; - import icon from '~/vue_shared/components/icon.vue'; - import modal from '~/vue_shared/components/modal.vue'; - import eventHub from '../event_hub'; - import { COMMON_STR } from '../constants'; +import tooltip from '~/vue_shared/directives/tooltip'; +import icon from '~/vue_shared/components/icon.vue'; +import eventHub from '../event_hub'; +import { COMMON_STR } from '../constants'; - export default { - components: { - icon, - modal, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + parentGroup: { + type: Object, + required: false, + default: () => ({}), }, - directives: { - tooltip, + group: { + type: Object, + required: true, }, - props: { - parentGroup: { - type: Object, - required: false, - default: () => ({}), - }, - group: { - type: Object, - required: true, - }, + }, + computed: { + leaveBtnTitle() { + return COMMON_STR.LEAVE_BTN_TITLE; }, - data() { - return { - modalStatus: false, - }; + editBtnTitle() { + return COMMON_STR.EDIT_BTN_TITLE; }, - computed: { - leaveBtnTitle() { - return COMMON_STR.LEAVE_BTN_TITLE; - }, - editBtnTitle() { - return COMMON_STR.EDIT_BTN_TITLE; - }, - leaveConfirmationMessage() { - return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); - }, + }, + methods: { + onLeaveGroup() { + eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup); }, - methods: { - onLeaveGroup() { - this.modalStatus = true; - }, - leaveGroup() { - this.modalStatus = false; - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - }, - }, - }; + }, +}; </script> <template> @@ -78,14 +63,5 @@ class="leave-group btn no-expand"> <icon name="leave"/> </a> - <modal - v-show="modalStatus" - :primary-button-label="__('Leave')" - kind="warning" - :title="__('Are you sure?')" - :text="__('Are you sure you want to leave this group?')" - :body="leaveConfirmationMessage" - @submit="leaveGroup" - /> </div> </template> diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index d2d3a257c0d..d87e6304a24 100644 --- a/app/assets/javascripts/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -1,4 +1,4 @@ -import { truncate } from './lib/utils/text_utility'; +import { truncate } from '../../../lib/utils/text_utility'; const MAX_MESSAGE_LENGTH = 500; const MESSAGE_CELL_SELECTOR = '.abuse-reports .message'; diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js new file mode 100644 index 00000000000..c0b6e8d4095 --- /dev/null +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -0,0 +1,3 @@ +import AbuseReports from './abuse_reports'; + +export default () => new AbuseReports(); diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/pages/admin/admin.js index c1f7fa2aced..135c15c346b 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -1,4 +1,4 @@ -import { refreshCurrentPage } from './lib/utils/url_utility'; +import { refreshCurrentPage } from '../../lib/utils/url_utility'; function showBlacklistType() { if ($('input[name="blacklist_type"]:checked').val() === 'file') { diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index ff88083a4b4..ff88083a4b4 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js new file mode 100644 index 00000000000..b548c48282a --- /dev/null +++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js @@ -0,0 +1,3 @@ +import initBroadcastMessagesForm from './broadcast_message'; + +export default () => initBroadcastMessagesForm(); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js new file mode 100644 index 00000000000..42ef9d38ef7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -0,0 +1,3 @@ +import initUsagePing from './usage_ping'; + +export default () => initUsagePing(); diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js index 2389056bd02..2389056bd02 100644 --- a/app/assets/javascripts/usage_ping.js +++ b/app/assets/javascripts/pages/admin/cohorts/usage_ping.js diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js new file mode 100644 index 00000000000..ff9ef8d2449 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/edit/index.js @@ -0,0 +1,3 @@ +import groupAvatar from '../../../../group_avatar'; + +export default () => groupAvatar(); diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js new file mode 100644 index 00000000000..fb5c46e4729 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/new/index.js @@ -0,0 +1,9 @@ +import BindInOut from '../../../../behaviors/bind_in_out'; +import Group from '../../../../group'; +import groupAvatar from '../../../../group_avatar'; + +export default () => { + BindInOut.initAll(); + new Group(); // eslint-disable-line no-new + groupAvatar(); +}; diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js new file mode 100644 index 00000000000..8b843037d85 --- /dev/null +++ b/app/assets/javascripts/pages/admin/index.js @@ -0,0 +1,3 @@ +import initAdmin from './admin'; + +export default () => initAdmin(); diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/edit/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js new file mode 100644 index 00000000000..d7ec6e47f67 --- /dev/null +++ b/app/assets/javascripts/pages/admin/labels/new/index.js @@ -0,0 +1,3 @@ +import Labels from '../../../../labels'; + +export default () => new Labels(); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js new file mode 100644 index 00000000000..71e0ddcd7b6 --- /dev/null +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -0,0 +1,9 @@ +import ProjectsList from '../../../projects_list'; +import NamespaceSelect from '../../../namespace_select'; + +export default () => { + new ProjectsList(); // eslint-disable-line no-new + + document.querySelectorAll('.js-namespace-select') + .forEach(dropdown => new NamespaceSelect({ dropdown })); +}; diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js new file mode 100644 index 00000000000..95faf1f1e98 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/activity/index.js @@ -0,0 +1,3 @@ +import Activities from '~/activities'; + +export default () => new Activities(); diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js new file mode 100644 index 00000000000..859b073f1cb --- /dev/null +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -0,0 +1,14 @@ +import GroupsList from '~/groups_list'; +import Landing from '~/landing'; + +export default function () { + new GroupsList(); // eslint-disable-line no-new + const landingElement = document.querySelector('.js-explore-groups-landing'); + if (!landingElement) return; + const exploreGroupsLanding = new Landing( + landingElement, + landingElement.querySelector('.dismiss-button'), + 'explore_groups_landing_dismissed', + ); + exploreGroupsLanding.toggle(); +} diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/help/index.js b/app/assets/javascripts/pages/help/index.js new file mode 100644 index 00000000000..4cf8afc4b7e --- /dev/null +++ b/app/assets/javascripts/pages/help/index.js @@ -0,0 +1,3 @@ +import VersionCheckImage from '../../version_check_image'; + +export default () => VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js new file mode 100644 index 00000000000..90eed38777a --- /dev/null +++ b/app/assets/javascripts/pages/profiles/index/index.js @@ -0,0 +1,7 @@ +import NotificationsForm from '../../../notifications_form'; +import notificationsDropdown from '../../../notifications_dropdown'; + +export default () => { + new NotificationsForm(); // eslint-disable-line no-new + notificationsDropdown(); +}; diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js new file mode 100644 index 00000000000..030328a1363 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -0,0 +1,3 @@ +import DueDateSelectors from '../../../due_date_select'; + +export default () => new DueDateSelectors(); diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js new file mode 100644 index 00000000000..7af95127fd5 --- /dev/null +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -0,0 +1,7 @@ +import Activities from '~/activities'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new Activities(); // eslint-disable-line no-new + new ShortcutsNavigation(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js new file mode 100644 index 00000000000..02456071086 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -0,0 +1,7 @@ +import BuildArtifacts from '~/build_artifacts'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new ShortcutsNavigation(); // eslint-disable-line no-new + new BuildArtifacts(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js new file mode 100644 index 00000000000..4cd67ac76e3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -0,0 +1,7 @@ +import BlobViewer from '~/blob/viewer/index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default function () { + new ShortcutsNavigation(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new +} diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js new file mode 100644 index 00000000000..060a78b427e --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js @@ -0,0 +1,16 @@ +import Pipelines from '../../../../pipelines'; + +export default () => { + const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + + new Pipelines({ // eslint-disable-line no-new + initTabs: true, + pipelineStatusUrl, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js new file mode 100644 index 00000000000..c54cc62bf05 --- /dev/null +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -0,0 +1,5 @@ +import NewBranchForm from '../../../../new_branch_form'; + +export default () => { + new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js new file mode 100644 index 00000000000..f4643e7dba0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -0,0 +1,12 @@ +import memberExpirationDate from '../../../member_expiration_date'; +import UsersSelect from '../../../users_select'; +import groupsSelect from '../../../groups_select'; +import Members from '../../../members'; + +export default () => { + memberExpirationDate('.js-access-expiration-date-groups'); + groupsSelect(); + memberExpirationDate(); + new Members(); // eslint-disable-line no-new + new UsersSelect(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js new file mode 100644 index 00000000000..4264c5c9dbe --- /dev/null +++ b/app/assets/javascripts/pages/search/show/index.js @@ -0,0 +1,3 @@ +import Search from './search'; + +export default () => new Search(); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/pages/search/show/search.js index 363322af47a..d44195f6b72 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -1,5 +1,5 @@ -import Flash from './flash'; -import Api from './api'; +import Flash from '~/flash'; +import Api from '~/api'; export default class Search { constructor() { diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js new file mode 100644 index 00000000000..f163557babc --- /dev/null +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -0,0 +1,11 @@ +import UsernameValidator from './username_validator'; +import SigninTabsMemoizer from './signin_tabs_memoizer'; +import OAuthRememberMe from './oauth_remember_me'; + +export default () => { + new UsernameValidator(); // eslint-disable-line no-new + new SigninTabsMemoizer(); // eslint-disable-line no-new + new OAuthRememberMe({ // eslint-disable-line no-new + container: $('.omniauth-container'), + }).bindEvents(); +}; diff --git a/app/assets/javascripts/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index ffc2dd6bbca..ffc2dd6bbca 100644 --- a/app/assets/javascripts/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 20255398047..f99573e5c74 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -1,6 +1,6 @@ /* eslint no-param-reassign: ["error", { "props": false }]*/ /* eslint no-new: "off" */ -import AccessorUtilities from './lib/utils/accessor'; +import AccessorUtilities from '~/lib/utils/accessor'; /** * Memorize the last selected tab after reloading a page. diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index bb34d5d2008..bb34d5d2008 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index e18852af6e9..31d9b9d9c48 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -1,10 +1,14 @@ <script> + import tooltip from '../directives/tooltip'; /** * Falls back to the code used in `copy_to_clipboard.js` */ export default { name: 'ClipboardButton', + directives: { + tooltip, + }, props: { text: { type: String, @@ -14,6 +18,16 @@ type: String, required: true, }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + tooltipContainer: { + type: [String, Boolean], + required: false, + default: false, + }, }, }; </script> @@ -22,8 +36,11 @@ <button type="button" class="btn btn-transparent btn-clipboard" - :data-title="title" + :title="title" :data-clipboard-text="text" + v-tooltip + :data-container="tooltipContainer" + :data-placement="tooltipPlacement" > <i aria-hidden="true" diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1588036aeae..1e91db5af9b 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -18,14 +18,9 @@ margin: $gl-padding 0; &.limited-width-container .file-content { - max-width: $limited-layout-width-sm; + max-width: $limited-layout-width; margin-left: auto; margin-right: auto; - - @media (min-width: $screen-md-min) { - padding-top: 64px; - padding-bottom: 64px; - } } } @@ -128,7 +123,7 @@ } &.wiki { - padding: 30px $gl-padding; + padding: $gl-padding; } &.blob-no-preview { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6cb32408a48..acbd9936706 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -16,12 +16,6 @@ display: inline-block; } -@media (min-width: $screen-md-min) { - .blob-viewer[data-type="rich"] { - margin: 20px; - } -} - .ide-view { display: flex; height: calc(100vh - #{$header-height}); diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 38b808cdc31..4b01904f2a1 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -65,6 +65,7 @@ class Admin::RunnersController < Admin::ApplicationController else Project.all end + @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? @projects = @projects.page(params[:page]).per(30) end diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb index b10147835f3..b569029283f 100644 --- a/app/controllers/concerns/group_tree.rb +++ b/app/controllers/concerns/group_tree.rb @@ -8,6 +8,7 @@ module GroupTree # Only show root groups if no parent-id is given groups.where(parent_id: params[:parent_id]) end + @groups = @groups.with_selects_for_list(archived: params[:archived]) .sort(@sort = params[:sort]) .page(params[:page]) diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index 4199da9cdf5..f745deb083c 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -32,6 +32,7 @@ module RoutableActions if canonical_path.casecmp(requested_full_path) != 0 flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." end + redirect_to build_canonical_path(routable) end end diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index d81ad135198..33b682d2859 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -12,6 +12,7 @@ class MetricsController < ActionController::Base ) "# Metrics are disabled, see: #{help_page}\n" end + render text: response, content_type: 'text/plain; version=0.0.4' end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index e3c18cba1dd..689d2e3db22 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -83,6 +83,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if ticket handle_service_ticket oauth['provider'], ticket end + handle_omniauth end @@ -90,6 +91,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if params['sid'] handle_service_ticket oauth['provider'], params['sid'] end + handle_omniauth end @@ -124,6 +126,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Only allow properly saved users to login. if @user.persisted? && @user.valid? log_audit_event(@user, with: oauth['provider']) + if @user.two_factor_enabled? params[:remember_me] = '1' if remember_me? prompt_for_two_factor(@user) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index d838b8dc29e..35e67730a27 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -150,6 +150,7 @@ class Projects::BlobController < Projects::ApplicationController if params[:file].present? params[:file_name] = params[:file].original_filename end + File.join(@path, params[:file_name]) elsif params[:file_path].present? params[:file_path] diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb index 25608df0b9c..4fc515bd03e 100644 --- a/app/controllers/projects/clusters/gcp_controller.rb +++ b/app/controllers/projects/clusters/gcp_controller.rb @@ -1,8 +1,9 @@ class Projects::Clusters::GcpController < Projects::ApplicationController before_action :authorize_read_cluster! before_action :authorize_google_api, except: [:login] - before_action :authorize_google_project_billing, only: [:new] + before_action :authorize_google_project_billing, only: [:new, :create] before_action :authorize_create_cluster!, only: [:new, :create] + before_action :verify_billing, only: [:create] def login begin @@ -23,24 +24,34 @@ class Projects::Clusters::GcpController < Projects::ApplicationController end def create + @cluster = ::Clusters::CreateService + .new(project, current_user, create_params) + .execute(token_in_session) + + if @cluster.persisted? + redirect_to project_cluster_path(project, @cluster) + else + render :new + end + end + + private + + def verify_billing case google_project_billing_status when 'true' - @cluster = ::Clusters::CreateService - .new(project, current_user, create_params) - .execute(token_in_session) - - return redirect_to project_cluster_path(project, @cluster) if @cluster.persisted? + return when 'false' - flash[:error] = _('Please enable billing for one of your projects to be able to create a cluster.') + flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" } else - flash[:error] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') + flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') end + @cluster = ::Clusters::Cluster.new(create_params) + render :new end - private - def create_params params.require(:cluster).permit( :enabled, diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index cf8829ba95b..e06dda1baa4 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -27,6 +27,7 @@ class Projects::DeployKeysController < Projects::ApplicationController unless @key.valid? && @project.deploy_keys << @key flash[:alert] = @key.errors.full_messages.join(', ').html_safe end + redirect_to_repository_settings(@project) end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 85d35900c71..6f51e7b9b40 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -21,6 +21,7 @@ class Projects::HooksController < Projects::ApplicationController @hooks = @project.hooks.select(&:persisted?) flash[:alert] = @hook.errors.full_messages.join.html_safe end + redirect_to project_settings_integrations_path(@project) end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index dc524b790a0..3d2926d5d75 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -48,6 +48,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap else [] end + @diff_notes_disabled = true @environment = @merge_request.environments_for(current_user).last diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6f229b08c0c..e6e2b219e6a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -203,6 +203,7 @@ class ProjectsController < Projects::ApplicationController else flash[:alert] = _("Project export could not be deleted.") end + redirect_to(edit_project_path(@project)) end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index d79108c88fb..c73306a6b66 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -28,6 +28,7 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end + # hide the signed-in notification flash[:notice] = nil log_audit_event(current_user, resource, with: authentication_method) diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 1a5f6063437..58570a580f1 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -63,6 +63,7 @@ class GroupDescendantsFinder groups_table = Group.arel_table visible_to_user = groups_table[:visibility_level] .in(Gitlab::VisibilityLevel.levels_for_user(current_user)) + if current_user authorized_groups = GroupsFinder.new(current_user, all_available: false) @@ -115,6 +116,7 @@ class GroupDescendantsFinder else direct_child_groups end + groups.with_selects_for_list(archived: params[:archived]).order_by(sort) end @@ -140,6 +142,7 @@ class GroupDescendantsFinder else direct_child_projects end + projects.with_route.order_by(sort) end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 6e8733bb49c..f2d3b90b8e2 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -34,6 +34,7 @@ class GroupProjectsFinder < ProjectsFinder else collection_without_user end + union(projects) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index f9dcb32f7c4..5e3b2e5581c 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -46,7 +46,7 @@ module BlobHelper end def ide_edit_text - "#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe + "#{_('Web IDE')}" end def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index f78d41a0448..2fe1927a189 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -203,6 +203,7 @@ module MarkupHelper node.content = node.content.truncate(num_remaining) truncated = true end + content_length += node.content.length end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 8ada746b244..680ea96a556 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -12,6 +12,7 @@ module NavHelper current_path?('projects/merge_requests/conflicts#show') || current_path?('issues#show') || current_path?('milestones#show') + if cookies[:collapsed_gutter] == 'true' %w[page-gutter right-sidebar-collapsed] else diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index b447d4952e7..00e7e4230b9 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -89,6 +89,7 @@ module SnippetsHelper snippet_chunk = [lined_content[line_number]] snippet_start_line = line_number end + last_line = line_number end # Add final chunk to chunk array diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 40d69e30188..1db9ae3839c 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -58,6 +58,7 @@ module SubmoduleHelper url_no_dotgit = url.chomp('.git') return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/', project].join('') + url_with_dotgit = url_no_dotgit + '.git' url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join('')) end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2a7aa299e83..e7c953e749e 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -30,6 +30,7 @@ module TodosHelper else todo.target_reference end + link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 8ab338d873d..80bda7f22ff 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -418,6 +418,7 @@ class ApplicationSetting < ActiveRecord::Base super(group_full_path) Gitlab::PerformanceBar.expire_allowed_user_ids_cache end + return end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 10ead6b6d3b..b6abc3d7681 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -2,8 +2,9 @@ module Ci class PipelineSchedule < ActiveRecord::Base extend Gitlab::Ci::Model include Importable + include IgnorableColumn - acts_as_paranoid + ignore_column :deleted_at belongs_to :project belongs_to :owner, class_name: 'User' diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index b5290bcaf53..aa065e33739 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,8 +1,9 @@ module Ci class Trigger < ActiveRecord::Base extend Gitlab::Ci::Model + include IgnorableColumn - acts_as_paranoid + ignore_column :deleted_at belongs_to :project belongs_to :owner, class_name: "User" diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index a3d0ac8d862..01079fb8bd6 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -10,7 +10,6 @@ module InternalId if iid.blank? parent = project || group records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend - records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) self.iid = max_iid.to_i + 1 diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 4251561a0a0..7049f340c9d 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -314,6 +314,7 @@ module Issuable includes = [] includes << :author unless notes.authors_loaded? includes << :award_emoji unless notes.award_emojis_loaded? + if includes.any? notes.includes(includes) else diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb index dcb3b2b5ff3..935e9d10133 100644 --- a/app/models/concerns/loaded_in_group_list.rb +++ b/app/models/concerns/loaded_in_group_list.rb @@ -25,6 +25,7 @@ module LoadedInGroupList base_count = projects.project(Arel.star.count.as('preloaded_project_count')) .where(projects[:namespace_id].eq(namespaces[:id])) + if archived == 'only' base_count.where(projects[:archived].eq(true)) elsif Gitlab::Utils.to_boolean(archived) diff --git a/app/models/issue.rb b/app/models/issue.rb index ad4a3c737ff..93628b456f2 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -12,7 +12,7 @@ class Issue < ActiveRecord::Base include ThrottledTouch include IgnorableColumn - ignore_column :assignee_id, :branch_name + ignore_column :assignee_id, :branch_name, :deleted_at DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze @@ -78,8 +78,6 @@ class Issue < ActiveRecord::Base end end - acts_as_paranoid - class << self alias_method :in_parents, :in_projects end diff --git a/app/models/label.rb b/app/models/label.rb index b5bfa6ea2dd..7538f2d8718 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -132,6 +132,7 @@ class Label < ActiveRecord::Base else priorities.find_by(project: project) end + priority.try(:priority) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ef58816937c..2669d2a6ff3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -11,7 +11,8 @@ class MergeRequest < ActiveRecord::Base include Gitlab::Utils::StrongMemoize ignore_column :locked_at, - :ref_fetched + :ref_fetched, + :deleted_at belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" @@ -150,8 +151,6 @@ class MergeRequest < ActiveRecord::Base after_save :keep_around_commit - acts_as_paranoid - def self.reference_prefix '!' end @@ -794,6 +793,7 @@ class MergeRequest < ActiveRecord::Base if !include_description && closes_issues_references.present? message << "Closes #{closes_issues_references.to_sentence}" end + message << "#{description}" if include_description && description.present? message << "See merge request #{to_reference(full: true)}" diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index e35de9b97ee..afab72930c1 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -49,6 +49,7 @@ class MergeRequestDiff < ActiveRecord::Base ensure_commit_shas save_commits save_diffs + save keep_around_commits end @@ -56,7 +57,6 @@ class MergeRequestDiff < ActiveRecord::Base self.start_commit_sha ||= merge_request.target_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha self.base_commit_sha ||= find_base_sha - save end # Override head_commit_sha to keep compatibility with merge request diff @@ -195,7 +195,7 @@ class MergeRequestDiff < ActiveRecord::Base end def commits_count - merge_request_diff_commits.size + super || merge_request_diff_commits.size end private @@ -264,13 +264,16 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:state] = :overflow if diff_collection.overflow? end - update(new_attributes) + assign_attributes(new_attributes) end def save_commits MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse) - merge_request_diff_commits.reload + # merge_request_diff_commits.reload is preferred way to reload associated + # objects but it returns cached result for some reason in this case + commits = merge_request_diff_commits(true) + self.commits_count = commits.size end def repository diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bdcc9159d26..37a7417cafc 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,6 +1,4 @@ class Namespace < ActiveRecord::Base - acts_as_paranoid without_default_scope: true - include CacheMarkdownField include Sortable include Gitlab::ShellAdapter @@ -10,6 +8,9 @@ class Namespace < ActiveRecord::Base include AfterCommitQueue include Storage::LegacyNamespace include Gitlab::SQL::Pattern + include IgnorableColumn + + ignore_column :deleted_at # Prevent users from creating unreasonably deep level of nesting. # The number 20 was taken based on maximum nesting level of @@ -221,12 +222,6 @@ class Namespace < ActiveRecord::Base has_parent? end - def soft_delete_without_removing_associations - # We can't use paranoia's `#destroy` since this will hard-delete projects. - # Project uses `pending_delete` instead of the acts_as_paranoia gem. - self.deleted_at = Time.now - end - private def refresh_access_of_projects_invited_groups diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index aec7b01e23a..c351d2012c6 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -224,6 +224,7 @@ module Network space_base = parents.first.space end end + space_base end diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 183e098d819..ab5a96209c7 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -9,6 +9,7 @@ class NotificationRecipient group: nil, skip_read_ability: false ) + unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" end diff --git a/app/models/project.rb b/app/models/project.rb index 7dc5e980c1b..029f2da2e4e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -633,6 +633,7 @@ class Project < ActiveRecord::Base project_import_data.data ||= {} project_import_data.data = project_import_data.data.merge(data) end + if credentials project_import_data.credentials ||= {} project_import_data.credentials = project_import_data.credentials.merge(credentials) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 768f0a7472e..bfe7ac29c18 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -110,6 +110,7 @@ class HipchatService < Service message = "" message << "#{push[:user_name]} " + if Gitlab::Git.blank_ref?(before) message << "pushed new #{ref_type} <a href=\""\ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\ diff --git a/app/models/repository.rb b/app/models/repository.rb index a84d6a1426b..d27212b2058 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1014,6 +1014,7 @@ class Repository else cache.fetch(key, &block) end + instance_variable_set(ivar, value) rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository # Even if the above `#exists?` check passes these errors might still diff --git a/app/models/service.rb b/app/models/service.rb index 24ba3039707..7f260f7a96b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -250,6 +250,7 @@ class Service < ActiveRecord::Base teamcity microsoft_teams ] + if Rails.env.development? service_names += %w[mock_ci mock_deployment mock_monitoring] end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 0bdd4d7a272..b5e2334b6e3 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -6,7 +6,6 @@ class IssueEntity < IssuableEntity expose :updated_by_id expose :created_at expose :updated_at - expose :deleted_at expose :milestone, using: API::Entities::Milestone expose :labels, using: LabelEntity expose :lock_version diff --git a/app/services/check_gcp_project_billing_service.rb b/app/services/check_gcp_project_billing_service.rb index 854adf2177d..ea82b61b279 100644 --- a/app/services/check_gcp_project_billing_service.rb +++ b/app/services/check_gcp_project_billing_service.rb @@ -2,7 +2,10 @@ class CheckGcpProjectBillingService def execute(token) client = GoogleApi::CloudPlatform::Client.new(token, nil) client.projects_list.select do |project| - client.projects_get_billing_info(project.name).billingEnabled + begin + client.projects_get_billing_info(project.project_id).billing_enabled + rescue + end end end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb index 63b85c3de7d..88dfb7a4a90 100644 --- a/app/services/create_deployment_service.rb +++ b/app/services/create_deployment_service.rb @@ -16,6 +16,7 @@ class CreateDeploymentService ActiveRecord::Base.transaction do environment.external_url = expanded_environment_url if expanded_environment_url + environment.fire_state_event(action) return unless environment.save diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index e3f9d9ee95d..58e88688dfa 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -1,7 +1,6 @@ module Groups class DestroyService < Groups::BaseService def async_execute - group.soft_delete_without_removing_associations job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end @@ -23,7 +22,7 @@ module Groups group.chat_team&.remove_mattermost_team(current_user) - group.really_destroy! + group.destroy end end end diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 29def25719d..2f511ab44b7 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -24,7 +24,7 @@ module Issues @new_issue = create_new_issue rewrite_notes - rewrite_award_emoji + rewrite_issue_award_emoji add_note_moved_from # Old issue tasks @@ -76,7 +76,7 @@ module Issues end def rewrite_notes - @old_issue.notes.find_each do |note| + @old_issue.notes_with_associations.find_each do |note| new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, note: rewrite_content(new_note.note), @@ -84,13 +84,19 @@ module Issues updated_at: note.updated_at } new_note.update(new_params) + + rewrite_award_emoji(note, new_note) end end - def rewrite_award_emoji - @old_issue.award_emoji.each do |award| + def rewrite_issue_award_emoji + rewrite_award_emoji(@old_issue, @new_issue) + end + + def rewrite_award_emoji(old_awardable, new_awardable) + old_awardable.award_emoji.each do |award| new_award = award.dup - new_award.awardable = @new_issue + new_award.awardable = new_awardable new_award.save end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 9622a5c5462..22b9b91a957 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -154,13 +154,9 @@ module MergeRequests end def assign_title_from_issue - return unless issue + return unless issue && issue.is_a?(Issue) - merge_request.title = - case issue - when Issue then "Resolve \"#{issue.title}\"" - when ExternalIssue then "Resolve #{issue.title}" - end + merge_request.title = "Resolve \"#{issue.title}\"" end def issue_iid diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 0d5a25fa28e..c0083cd6afd 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -1,12 +1,14 @@ module MergeRequests class RebaseService < MergeRequests::WorkingCopyBaseService + REBASE_ERROR = 'Rebase failed. Please rebase locally'.freeze + def execute(merge_request) @merge_request = merge_request if rebase success else - error('Failed to rebase. Should be done manually') + error(REBASE_ERROR) end end @@ -22,8 +24,8 @@ module MergeRequests true rescue => e - log_error('Failed to rebase branch:') - log_error(e.message, save_message_on_model: true) + log_error(REBASE_ERROR, save_message_on_model: true) + log_error(e.message) false end end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 00db8a2c434..b71002433d6 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -53,7 +53,7 @@ module Users # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing user_data = user.destroy - namespace.really_destroy! + namespace.destroy user_data end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 39eb71c2bac..46727811be4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-gitlab +%header.navbar.navbar-gitlab.qa-navbar %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content @@ -43,7 +43,7 @@ = todos_count_format(todos_pending_count) %li.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do - = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar" + = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu-nav.dropdown-menu-align-right %ul @@ -56,8 +56,6 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path - %li - = link_to "Turn on multi edit", profile_preferences_path - if current_user %li = link_to "Help", help_path diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4013181da9c..74532eba298 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,5 +1,5 @@ %ul.list-unstyled.navbar-sub-nav - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do %a{ href: "#", data: { toggle: "dropdown" } } Projects = sprite_icon('angle-down', css_class: 'caret-down') @@ -7,7 +7,7 @@ = render "layouts/nav/projects_dropdown/show" = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do - = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do + = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do Groups = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do @@ -59,7 +59,7 @@ %li.line-separator.hidden-xs - if current_user.admin? = nav_link(controller: 'admin/dashboard') do - = link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('admin', size: 18) - if Gitlab::Sherlock.enabled? %li diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index 32a24c101fc..59becb043d3 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,9 +1,9 @@ - project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container - .project-dropdown-sidebar + .project-dropdown-sidebar.qa-projects-dropdown-sidebar %ul = nav_link(path: 'dashboard/projects#index') do - = link_to dashboard_projects_path do + = link_to dashboard_projects_path, class: 'qa-your-projects-link' do = _('Your projects') = nav_link(path: 'projects#starred') do = link_to starred_dashboard_projects_path do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 1fa3a3041fd..abd07d71bcc 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -226,7 +226,7 @@ = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name + %span.nav-item-name.qa-settings-item Settings %ul.sidebar-sub-level-items diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 65328791ce5..aeae7455a1c 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -5,8 +5,8 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-4 %h4.prepend-top-0 - GitLab multi file editor - %p Unlock an additional editing experience which makes it possible to edit and commit multiple files + Web IDE (Beta) + %p Enable the new web IDE on this device to make it possible to open and edit multiple files with a single commit .col-lg-8.multi-file-editor-options = label_tag do .preview.append-bottom-10= image_tag "multi-editor-off.png" diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1d644dda177..b565f14747a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -4,7 +4,7 @@ .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') - %h1.project-title + %h1.project-title.qa-project-name = @project.name %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false) diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index a78a8e5d628..bd99eb93cc8 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -9,7 +9,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} + = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1} - else .input-group-addon.static-namespace diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 2589c53beae..8e8c911185a 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -30,12 +30,13 @@ %li = link_to project_new_blob_path(@project, @project.default_branch || 'master') do #{ _('New file') } - %li - = link_to new_project_branch_path(@project) do - #{ _('New branch') } - %li - = link_to new_project_tag_path(@project) do - #{ _('New tag') } + - unless @project.empty_repo? + %li + = link_to new_project_branch_path(@project) do + #{ _('New branch') } + %li + = link_to new_project_tag_path(@project) do + #{ _('New tag') } - elsif current_user && current_user.already_forked?(@project) %li = link_to project_new_blob_path(@project, @project.default_branch || 'master') do diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml index e2d7326a312..bddb902115d 100644 --- a/app/views/projects/clusters/gcp/_header.html.haml +++ b/app/views/projects/clusters/gcp/_header.html.haml @@ -4,11 +4,11 @@ = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') %ul %li - - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') + - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine } %li - - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') + - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements } %li - - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer') + - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index c7c84b5a42c..2049105dff6 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -1,6 +1,6 @@ - @content_class = "limit-container-width" unless fluid_layout - add_to_breadcrumbs "Clusters", project_clusters_path(@project) -- breadcrumb_title @cluster.id +- breadcrumb_title @cluster.name - page_title _("Cluster") - expanded = Rails.env.test? diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 9fc297ab7f6..5dd4d2c949c 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -27,7 +27,7 @@ Edit - if @project.group - = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do + = link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "You are about to promote #{@milestone.title} to a group level. This will make this milestone available to all projects inside #{@project.group.name}. The existing project milestone will be merged into the group level. This action cannot be reversed.", toggle: "tooltip" }, method: :post do Promote - if @milestone.active? diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 1cba4fc6c41..687cd4d1532 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -7,7 +7,7 @@ %span = enabled_project_button(project, enabled_protocol) - else - %a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } } + %a#clone-dropdown.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } %span = default_clone_protocol.upcase = icon('caret-down') diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 81d07074325..8e88cecaf9e 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -77,7 +77,7 @@ = icon('spinner spin', class: 'label-subscribe-button-loading') - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do + = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do %span.sr-only Promote to Group = icon('level-up') - if can?(current_user, :admin_label, label) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 7ba8f9d4313..50f4901a2dd 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -51,7 +51,7 @@ \ - if @project.group - = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting this milestone will make it available for all projects inside the group. Existing project milestones with the same name will be merged. Are you sure?", toggle: "tooltip" }, method: :post do + = link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "You are about to promote #{milestone.title} to a group level. This will make this milestone available to all projects inside #{@project.group.name}. The existing project milestone will be merged into the group level. This action cannot be reversed.", toggle: "tooltip" }, method: :post do Promote = 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" diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb index 557af14ee57..5466ccdda59 100644 --- a/app/workers/check_gcp_project_billing_worker.rb +++ b/app/workers/check_gcp_project_billing_worker.rb @@ -4,7 +4,7 @@ class CheckGcpProjectBillingWorker include ApplicationWorker include ClusterQueue - LEASE_TIMEOUT = 15.seconds.to_i + LEASE_TIMEOUT = 3.seconds.to_i SESSION_KEY_TIMEOUT = 5.minutes BILLING_TIMEOUT = 1.hour @@ -23,13 +23,13 @@ class CheckGcpProjectBillingWorker end def self.redis_shared_state_key_for(token) - "gitlab:gcp:#{token.hash}:billing_enabled" + "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" end def perform(token_key) return unless token_key - token = self.get_session_token(token_key) + token = self.class.get_session_token(token_key) return unless token return unless try_obtain_lease_for(token) diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index f577b310b20..509bd09dc2e 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -4,7 +4,7 @@ class GroupDestroyWorker def perform(group_id, user_id) begin - group = Group.with_deleted.find(group_id) + group = Group.find(group_id) rescue ActiveRecord::RecordNotFound return end diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 3ec81d040b4..d3b95009364 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -13,6 +13,7 @@ class PagesWorker if result[:status] == :success result = Projects::UpdatePagesConfigurationService.new(build.project).execute end + result end diff --git a/changelogs/unreleased/18040-line-breaks-around-conditional-blocks.yml b/changelogs/unreleased/18040-line-breaks-around-conditional-blocks.yml new file mode 100644 index 00000000000..447c65a3764 --- /dev/null +++ b/changelogs/unreleased/18040-line-breaks-around-conditional-blocks.yml @@ -0,0 +1,5 @@ +--- +title: Adds Rubocop rule for line break around conditionals +merge_request: 15739 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/36669-default-mr-title-with-external-issues.yml b/changelogs/unreleased/36669-default-mr-title-with-external-issues.yml new file mode 100644 index 00000000000..6af9ac4b099 --- /dev/null +++ b/changelogs/unreleased/36669-default-mr-title-with-external-issues.yml @@ -0,0 +1,5 @@ +--- +title: Default merge request title is set correctly again when external issue tracker is activated +merge_request: 16356 +author: Ben305 +type: fixed diff --git a/changelogs/unreleased/38068-commits-count.yml b/changelogs/unreleased/38068-commits-count.yml new file mode 100644 index 00000000000..3fbf554c98c --- /dev/null +++ b/changelogs/unreleased/38068-commits-count.yml @@ -0,0 +1,5 @@ +--- +title: Store number of commits in merge_request_diffs table. +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/39214__pipeline_api.yml b/changelogs/unreleased/39214__pipeline_api.yml new file mode 100644 index 00000000000..18ee2e43798 --- /dev/null +++ b/changelogs/unreleased/39214__pipeline_api.yml @@ -0,0 +1,5 @@ +--- +title: Add `pipelines` endpoint to merge requests API +merge_request: 15454 +author: Tony Rom <thetonyrom@gmail.com> +type: added diff --git a/changelogs/unreleased/39988-hide-new-branch-tag-empty-repo.yml b/changelogs/unreleased/39988-hide-new-branch-tag-empty-repo.yml new file mode 100644 index 00000000000..4f2c87c44b3 --- /dev/null +++ b/changelogs/unreleased/39988-hide-new-branch-tag-empty-repo.yml @@ -0,0 +1,5 @@ +--- +title: Hide new branch and tag links for projects with an empty repo +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/4020-rebase-message.yml b/changelogs/unreleased/4020-rebase-message.yml new file mode 100644 index 00000000000..4793f3d9cb9 --- /dev/null +++ b/changelogs/unreleased/4020-rebase-message.yml @@ -0,0 +1,5 @@ +--- +title: Display user friendly error message if rebase fails. +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/41600-wider-project-readme-on-fixed-layout.yml b/changelogs/unreleased/41600-wider-project-readme-on-fixed-layout.yml new file mode 100644 index 00000000000..e50f6046b17 --- /dev/null +++ b/changelogs/unreleased/41600-wider-project-readme-on-fixed-layout.yml @@ -0,0 +1,5 @@ +--- +title: Make project README containers wider on fixed layout +merge_request: 16181 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/41613-fix-redundant-modal.yml b/changelogs/unreleased/41613-fix-redundant-modal.yml new file mode 100644 index 00000000000..9e157b3065a --- /dev/null +++ b/changelogs/unreleased/41613-fix-redundant-modal.yml @@ -0,0 +1,5 @@ +--- +title: Make modal dialog common for Groups tree app +merge_request: 16311 +author: +type: fixed diff --git a/changelogs/unreleased/41709-rich-blob-viewer-margins-for-pc.yml b/changelogs/unreleased/41709-rich-blob-viewer-margins-for-pc.yml new file mode 100644 index 00000000000..51285e5476f --- /dev/null +++ b/changelogs/unreleased/41709-rich-blob-viewer-margins-for-pc.yml @@ -0,0 +1,5 @@ +--- +title: Make rich blob viewer wider for PC +merge_request: 16262 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/41789-fix-up-web-ide-user-preference-copy-and-buttons.yml b/changelogs/unreleased/41789-fix-up-web-ide-user-preference-copy-and-buttons.yml new file mode 100644 index 00000000000..fe87cd5cadb --- /dev/null +++ b/changelogs/unreleased/41789-fix-up-web-ide-user-preference-copy-and-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Fix web ide user preferences copy and buttons +merge_request: 41789 +author: +type: other diff --git a/changelogs/unreleased/remove-soft-removals.yml b/changelogs/unreleased/remove-soft-removals.yml new file mode 100644 index 00000000000..aa53d33e502 --- /dev/null +++ b/changelogs/unreleased/remove-soft-removals.yml @@ -0,0 +1,5 @@ +--- +title: Remove soft removals related code +merge_request: 15789 +author: +type: changed diff --git a/changelogs/unreleased/sh-fix-award-emoji-move-issues.yml b/changelogs/unreleased/sh-fix-award-emoji-move-issues.yml new file mode 100644 index 00000000000..c62fad927d0 --- /dev/null +++ b/changelogs/unreleased/sh-fix-award-emoji-move-issues.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug where award emojis would be lost when moving issues between projects +merge_request: +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 1110199b888..ea9a07cbde9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -61,6 +61,7 @@ module Gitlab # - Any parameter containing `secret` # - Two-factor tokens (:otp_attempt) # - Repo/Project Import URLs (:import_url) + # - Build traces (:trace) # - Build variables (:variables) # - GitLab Pages SSL cert/key info (:certificate, :encrypted_key) # - Webhook URLs (:hook) @@ -75,6 +76,7 @@ module Gitlab key otp_attempt sentry_dsn + trace variables ) @@ -149,6 +151,7 @@ module Gitlab caching_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5 caching_config_hash[:pool_timeout] = 1 end + config.cache_store = :redis_store, caching_config_hash config.active_record.raise_in_transactional_callbacks = true diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index f10f0cdf42c..abc992e49dc 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -68,6 +68,7 @@ class Settings < Settingslogic end values.delete_if { |value| value.nil? } end + values end @@ -78,6 +79,7 @@ class Settings < Settingslogic if current.is_a? String value = modul.const_get(current.upcase) rescue default end + value end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 051ef93b205..fa25f3778fa 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -241,6 +241,7 @@ Devise.setup do |config| true end end + if provider['name'] == 'authentiq' provider['args'][:remote_sign_out_handler] = lambda do |request| authentiq_session = request.params['sid'] diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index 581397b26f8..e74b95f1646 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -2,6 +2,7 @@ Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Re Peek.into Peek::Views::Host Peek.into Peek::Views::PerformanceBar + if Gitlab::Database.mysql? require 'peek-mysql2' PEEK_DB_CLIENT = ::Mysql2::Client @@ -11,6 +12,7 @@ else PEEK_DB_CLIENT = ::PG::Connection PEEK_DB_VIEW = Peek::Views::PG end + Peek.into PEEK_DB_VIEW Peek.into Peek::Views::Redis Peek.into Peek::Views::Sidekiq diff --git a/db/migrate/20170928124105_create_fork_networks.rb b/db/migrate/20170928124105_create_fork_networks.rb index ca906b953a3..89e5b871967 100644 --- a/db/migrate/20170928124105_create_fork_networks.rb +++ b/db/migrate/20170928124105_create_fork_networks.rb @@ -23,6 +23,7 @@ class CreateForkNetworks < ActiveRecord::Migration if foreign_keys_for(:fork_networks, :root_project_id).any? remove_foreign_key :fork_networks, column: :root_project_id end + drop_table :fork_networks end end diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb index 836f023efdc..8c7d9ba859a 100644 --- a/db/migrate/20170928133643_create_fork_network_members.rb +++ b/db/migrate/20170928133643_create_fork_network_members.rb @@ -21,6 +21,7 @@ class CreateForkNetworkMembers < ActiveRecord::Migration if foreign_keys_for(:fork_network_members, :forked_from_project_id).any? remove_foreign_key :fork_network_members, column: :forked_from_project_id end + drop_table :fork_network_members end end diff --git a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb index 7cf1d0cec68..d1a039ed551 100644 --- a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb +++ b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb @@ -9,6 +9,7 @@ class AddIndexOnNamespacesLowerName < ActiveRecord::Migration return unless Gitlab::Database.postgresql? disable_statement_timeout + if Gitlab::Database.version.to_f >= 9.5 # Allow us to hot-patch the index manually ahead of the migration execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));" @@ -21,6 +22,7 @@ class AddIndexOnNamespacesLowerName < ActiveRecord::Migration return unless Gitlab::Database.postgresql? disable_statement_timeout + if Gitlab::Database.version.to_f >= 9.2 execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" else diff --git a/db/migrate/20180105212544_add_commits_count_to_merge_request_diff.rb b/db/migrate/20180105212544_add_commits_count_to_merge_request_diff.rb new file mode 100644 index 00000000000..f942b4c062e --- /dev/null +++ b/db/migrate/20180105212544_add_commits_count_to_merge_request_diff.rb @@ -0,0 +1,29 @@ +class AddCommitsCountToMergeRequestDiff < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'AddMergeRequestDiffCommitsCount'.freeze + BATCH_SIZE = 5000 + DELAY_INTERVAL = 5.minutes.to_i + + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + + include ::EachBatch + end + + disable_ddl_transaction! + + def up + add_column :merge_request_diffs, :commits_count, :integer + + say 'Populating the MergeRequestDiff `commits_count`' + + queue_background_migration_jobs_by_range_at_intervals(MergeRequestDiff, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + remove_column :merge_request_diffs, :commits_count + end +end diff --git a/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb b/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb index da0fcda87a6..17ad7de065d 100644 --- a/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb +++ b/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb @@ -31,6 +31,7 @@ class RenameUsersWithRenamedNamespace < ActiveRecord::Migration predicate = namespaces[:owner_id].eq(users[:id]) .and(namespaces[:type].eq(nil)) .and(users[:username].matches(path)) + update_sql = if Gitlab::Database.postgresql? "UPDATE users SET username = namespaces.path "\ "FROM namespaces WHERE #{predicate.to_sql}" diff --git a/db/post_migrate/20171207150343_remove_soft_removed_objects.rb b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb new file mode 100644 index 00000000000..3e2dedfdd6a --- /dev/null +++ b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb @@ -0,0 +1,208 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveSoftRemovedObjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + module SoftRemoved + extend ActiveSupport::Concern + + included do + scope :soft_removed, -> { where('deleted_at IS NOT NULL') } + end + end + + class User < ActiveRecord::Base + self.table_name = 'users' + + include EachBatch + end + + class Issue < ActiveRecord::Base + self.table_name = 'issues' + + include EachBatch + include SoftRemoved + end + + class MergeRequest < ActiveRecord::Base + self.table_name = 'merge_requests' + + include EachBatch + include SoftRemoved + end + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + + include EachBatch + include SoftRemoved + + scope :soft_removed_personal, -> { soft_removed.where(type: nil) } + scope :soft_removed_group, -> { soft_removed.where(type: 'Group') } + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + + include EachBatch + include SoftRemoved + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + + include EachBatch + include SoftRemoved + end + + class CiPipelineSchedule < ActiveRecord::Base + self.table_name = 'ci_pipeline_schedules' + + include EachBatch + include SoftRemoved + end + + class CiTrigger < ActiveRecord::Base + self.table_name = 'ci_triggers' + + include EachBatch + include SoftRemoved + end + + MODELS = [Issue, MergeRequest, CiPipelineSchedule, CiTrigger].freeze + + def up + disable_statement_timeout + + remove_personal_routes + remove_personal_namespaces + remove_group_namespaces + remove_simple_soft_removed_rows + end + + def down + # The data removed by this migration can't be restored in an automated way. + end + + def remove_simple_soft_removed_rows + create_temporary_indexes + + MODELS.each do |model| + say_with_time("Removing soft removed rows from #{model.table_name}") do + model.soft_removed.each_batch do |batch, index| + batch.delete_all + end + end + end + ensure + remove_temporary_indexes + end + + def create_temporary_indexes + MODELS.each do |model| + index_name = temporary_index_name_for(model) + + # Without this index the removal process can take a very long time. For + # example, getting the next ID of a batch for the `issues` table in + # staging would take between 15 and 20 seconds. + next if temporary_index_exists?(model) + + say_with_time("Creating temporary index #{index_name}") do + add_concurrent_index( + model.table_name, + [:deleted_at, :id], + name: index_name, + where: 'deleted_at IS NOT NULL' + ) + end + end + end + + def remove_temporary_indexes + MODELS.each do |model| + index_name = temporary_index_name_for(model) + + next unless temporary_index_exists?(model) + + say_with_time("Removing temporary index #{index_name}") do + remove_concurrent_index_by_name(model.table_name, index_name) + end + end + end + + def temporary_index_name_for(model) + "index_on_#{model.table_name}_tmp" + end + + def temporary_index_exists?(model) + index_name = temporary_index_name_for(model) + + index_exists?(model.table_name, [:deleted_at, :id], name: index_name) + end + + def remove_personal_namespaces + # Some personal namespaces are left behind in case of GitLab.com. In these + # cases the associated data such as the projects and users has already been + # removed. + Namespace.soft_removed_personal.each_batch do |batch| + batch.delete_all + end + end + + def remove_group_namespaces + admin_id = id_for_admin_user + + unless admin_id + say 'Not scheduling soft removed groups for removal as no admin user ' \ + 'could be found. You will need to remove any such groups manually.' + + return + end + + # Left over groups can't be easily removed because we may also need to + # remove memberships, repositories, and other associated data. As a result + # we'll just schedule a Sidekiq job to remove these. + # + # As of January 5th, 2018 there are 36 groups that will be removed using + # this code. + Namespace.select(:id).soft_removed_group.each_batch(of: 10) do |batch, index| + batch.each do |ns| + schedule_group_removal(index * 5.minutes, ns.id, admin_id) + end + end + end + + def schedule_group_removal(delay, group_id, user_id) + if migrate_inline? + GroupDestroyWorker.new.perform(group_id, user_id) + else + GroupDestroyWorker.perform_in(delay, group_id, user_id) + end + end + + def remove_personal_routes + namespaces = Namespace.select(1) + .soft_removed + .where('namespaces.type IS NULL') + .where('routes.source_type = ?', 'Namespace') + .where('routes.source_id = namespaces.id') + + Route.where('EXISTS (?)', namespaces).each_batch do |batch| + batch.delete_all + end + end + + def id_for_admin_user + User.where(admin: true).limit(1).pluck(:id).first + end + + def migrate_inline? + Rails.env.test? || Rails.env.development? + end +end diff --git a/db/post_migrate/20171207150344_remove_deleted_at_columns.rb b/db/post_migrate/20171207150344_remove_deleted_at_columns.rb new file mode 100644 index 00000000000..154d7a1b926 --- /dev/null +++ b/db/post_migrate/20171207150344_remove_deleted_at_columns.rb @@ -0,0 +1,31 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDeletedAtColumns < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + TABLES = %i[issues merge_requests namespaces ci_pipeline_schedules ci_triggers].freeze + COLUMN = :deleted_at + + def up + TABLES.each do |table| + remove_column(table, COLUMN) if column_exists?(table, COLUMN) + end + end + + def down + TABLES.each do |table| + unless column_exists?(table, COLUMN) + add_column(table, COLUMN, :datetime_with_timezone) + end + + unless index_exists?(table, COLUMN) + add_concurrent_index(table, COLUMN) + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index a16f756ccfb..8a6db61250b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171230123729) do +ActiveRecord::Schema.define(version: 20180105212544) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -357,7 +357,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "project_id" t.integer "owner_id" t.boolean "active", default: true - t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" end @@ -467,7 +466,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do create_table "ci_triggers", force: :cascade do |t| t.string "token" - t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" t.integer "project_id" @@ -861,7 +859,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "iid" t.integer "updated_by_id" t.boolean "confidential", default: false, null: false - t.datetime "deleted_at" t.date "due_date" t.integer "moved_to_id" t.integer "lock_version" @@ -878,7 +875,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree - add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["moved_to_id"], name: "index_issues_on_moved_to_id", where: "(moved_to_id IS NOT NULL)", using: :btree @@ -1044,6 +1040,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.string "real_size" t.string "head_commit_sha" t.string "start_commit_sha" + t.integer "commits_count" end add_index "merge_request_diffs", ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id", using: :btree @@ -1087,7 +1084,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.boolean "merge_when_pipeline_succeeds", default: false, null: false t.integer "merge_user_id" t.string "merge_commit_sha" - t.datetime "deleted_at" t.string "in_progress_merge_commit_sha" t.integer "lock_version" t.text "title_html" @@ -1106,7 +1102,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree - add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["latest_merge_request_diff_id"], name: "index_merge_requests_on_latest_merge_request_diff_id", using: :btree @@ -1166,7 +1161,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.boolean "share_with_group_lock", default: false t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false - t.datetime "deleted_at" t.text "description_html" t.boolean "lfs_enabled" t.integer "parent_id" @@ -1176,7 +1170,6 @@ ActiveRecord::Schema.define(version: 20171230123729) do end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree - add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index e09ccaba08c..d8928a7fe4c 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -32,7 +32,9 @@ options: ## AWS Elastic File System -GitLab does not recommend using AWS Elastic File System (EFS). +GitLab strongly recommends against using AWS Elastic File System (EFS). +Our support team will not be able to assist on performance issues related to +file system access. Customers and users have reported that AWS EFS does not perform well for GitLab's use-case. There are several issues that can cause problems. For these reasons diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 24afcef9a31..22ccc6a46f3 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -468,6 +468,30 @@ Parameters: } ``` +## List MR pipelines + +Get a list of merge request pipelines. + +``` +GET /projects/:id/merge_requests/:merge_request_iid/pipelines +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `merge_request_iid` (required) - The internal ID of the merge request + +```json +[ + { + "id": 77, + "sha": "959e04d7c7a30600c894bd3c0cd0e1ce7f42c11d", + "ref": "master", + "status": "success" + } +] +``` + ## Create MR Creates a new merge request. diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md index 9030ae32d17..e881e61d4ef 100644 --- a/doc/api/pipeline_triggers.md +++ b/doc/api/pipeline_triggers.md @@ -24,7 +24,6 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/ "id": 10, "description": "my trigger", "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, "last_used": null, "token": "6d056f63e50fe6f8c5f8f4aa10edb7", "updated_at": "2016-01-07T09:53:58.235Z", @@ -55,7 +54,6 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/ "id": 10, "description": "my trigger", "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, "last_used": null, "token": "6d056f63e50fe6f8c5f8f4aa10edb7", "updated_at": "2016-01-07T09:53:58.235Z", @@ -85,7 +83,6 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form descri "id": 10, "description": "my trigger", "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, "last_used": null, "token": "6d056f63e50fe6f8c5f8f4aa10edb7", "updated_at": "2016-01-07T09:53:58.235Z", @@ -116,7 +113,6 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form descrip "id": 10, "description": "my trigger", "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, "last_used": null, "token": "6d056f63e50fe6f8c5f8f4aa10edb7", "updated_at": "2016-01-07T09:53:58.235Z", @@ -146,7 +142,6 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl "id": 10, "description": "my trigger", "created_at": "2016-01-07T09:53:58.235Z", - "deleted_at": null, "last_used": null, "token": "6d056f63e50fe6f8c5f8f4aa10edb7", "updated_at": "2016-01-07T09:53:58.235Z", diff --git a/doc/api/snippets.md b/doc/api/snippets.md index fdafbfb5b9e..42b760c107d 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -2,7 +2,7 @@ > [Introduced][ce-6373] in GitLab 8.15. -### Snippet visibility level +## Snippet visibility level Snippets in GitLab can be either private, internal, or public. You can set it with the `visibility` field in the snippet. @@ -84,7 +84,11 @@ Parameters: ``` bash -curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets +curl --request POST \ + --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \ + --header 'Content-Type: application/json' \ + --header "PRIVATE-TOKEN: valid_api_token" \ + https://gitlab.example.com/api/v4/snippets ``` Example response: @@ -131,7 +135,11 @@ Parameters: ``` bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v4/snippets/1 +curl --request PUT \ + --data '{"title": "foo", "content": "bar"}' \ + --header 'Content-Type: application/json' \ + --header "PRIVATE-TOKEN: valid_api_token" \ + https://gitlab.example.com/api/v4/snippets/1 ``` Example response: @@ -265,4 +273,5 @@ Example response: } ``` -[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508 +[ce-6373]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6373 +[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12655 diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index abe5b06e0f0..5b4f6511f04 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -25,7 +25,7 @@ It is possible to run end-to-end tests (eventually being run within a the `package-qa` manual action, that should be present in a merge request widget. -Mmanual action that starts end-to-end tests is also available in merge requests +Manual action that starts end-to-end tests is also available in merge requests in Omnibus GitLab project. Below you can read more about how to use it and how does it work. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 5f14d232cb1..130f7897b1a 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -22,11 +22,14 @@ prerequisites must be met: be enabled in GitLab at the instance level. If that's not the case, ask your administrator to enable it. - Your associated Google account must have the right privileges to manage - clusters on GKE. That would mean that a - [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) - must be set up. -- You must have Master [permissions] in order to be able to access the **Cluster** - page. + clusters on GKE. That would mean that a [billing + account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) + must be set up and that you have to have permissions to access it. +- You must have Master [permissions] in order to be able to access the + **Cluster** page. +- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled +- You must have [Resource Manager + API](https://cloud.google.com/resource-manager/) If all of the above requirements are met, you can proceed to add a new GKE cluster. diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index f530b6cb649..cc3218fbfd1 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -10,12 +10,7 @@ in the table below. | `description` | A name for the issue tracker (to differentiate between instances, for example) | | `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | | `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | - | `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | - - Once you have configured and enabled Redmine: - - the **Issues** link on the GitLab project pages takes you to the appropriate - Redmine issue index - - clicking **New issue** on the project dashboard creates a new Redmine issue + | `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** | As an example, below is a configuration for a project named gitlab-ci. diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index c623a516c47..bd3011b1cd8 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -180,11 +180,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps dropdown.find(".compare-dropdown-toggle").click dropdown.find('.dropdown-menu', visible: true) dropdown.fill_in("Filter by Git revision", with: selection) + if is_commit dropdown.find('input[type="search"]').send_keys(:return) else find_link(selection, visible: true).click end + dropdown.find('.dropdown-menu', visible: false) end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f574858be02..c4ef2c74658 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -918,7 +918,7 @@ module API class Trigger < Grape::Entity expose :id expose :token, :description - expose :created_at, :updated_at, :deleted_at, :last_used + expose :created_at, :updated_at, :last_used expose :owner, using: Entities::UserBasic end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d6ce368efd5..6134ad2bfc7 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -26,6 +26,7 @@ module API check_unmodified_since!(last_updated) status 204 + if block_given? yield resource else diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 8bf53939751..063f0d6599c 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -103,6 +103,7 @@ module API elsif params[:user_id] user = User.find_by(id: params[:user_id]) end + present user, with: Entities::UserSafe end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 7aa10631d53..c99fe3ab5b3 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -175,6 +175,7 @@ module API issue = ::Issues::CreateService.new(user_project, current_user, issue_params.merge(request: request, api: true)).execute + if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8f665b39fa8..420aaf1c964 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -24,6 +24,13 @@ module API .preload(:notes, :author, :assignee, :milestone, :latest_merge_request_diff, :labels, :timelogs) end + def merge_request_pipelines_with_access + authorize! :read_pipeline, user_project + + mr = find_merge_request_with_access(params[:merge_request_iid]) + mr.all_pipelines + end + params :merge_requests_params do optional :state, type: String, values: %w[opened closed merged all], default: 'all', desc: 'Return opened, closed, merged, or all merge requests' @@ -214,6 +221,15 @@ module API present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end + desc 'Get the merge request pipelines' do + success Entities::PipelineBasic + end + get ':id/merge_requests/:merge_request_iid/pipelines' do + pipelines = merge_request_pipelines_with_access + + present paginate(pipelines), with: Entities::PipelineBasic + end + desc 'Update a merge request' do success Entities::MergeRequest end diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 74b3376a1f3..675c963bae2 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -48,6 +48,7 @@ module API current_user, declared_params(include_missing: false)) .execute(:api, ignore_skip_ci: true, save_on_errors: false) + if new_pipeline.persisted? present new_pipeline, with: Entities::Pipeline else diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 2ccda1c1aa1..5bed58c2d63 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -13,6 +13,7 @@ module API if errors[:project_access].any? error!(errors[:project_access], 422) end + not_found! end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index fa222bf2b1c..653126e79ea 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -154,6 +154,7 @@ module API if project.errors[:limit_reached].present? error!(project.errors[:limit_reached], 403) end + render_validation_error!(project) end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 4f36bbd760f..9638c53a1df 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -15,6 +15,7 @@ module API if errors[:project_access].any? error!(errors[:project_access], 422) end + not_found! end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index c17b6f45ed8..64758dae7d3 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -207,7 +207,7 @@ module API end class Trigger < Grape::Entity - expose :token, :created_at, :updated_at, :deleted_at, :last_used + expose :token, :created_at, :updated_at, :last_used expose :owner, using: ::API::Entities::UserBasic end diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb index 684860b553e..de226e4e573 100644 --- a/lib/api/v3/members.rb +++ b/lib/api/v3/members.rb @@ -67,6 +67,7 @@ module API unless member member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) end + if member.persisted? && member.valid? present member.user, with: ::API::Entities::Member, member: member else diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 1d6d823f32b..0a24fea52a3 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -126,6 +126,7 @@ module API if status == :deprecated detail DEPRECATION_MESSAGE end + success ::API::V3::Entities::MergeRequest end get path do diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb index c41fee32610..6ba425ba8c7 100644 --- a/lib/api/v3/project_snippets.rb +++ b/lib/api/v3/project_snippets.rb @@ -14,6 +14,7 @@ module API if errors[:project_access].any? error!(errors[:project_access], 422) end + not_found! end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index 7c260b8d910..446f804124b 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -41,6 +41,7 @@ module API # private or internal, use the more conservative option, private. attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE end + attrs end @@ -201,6 +202,7 @@ module API if project.errors[:limit_reached].present? error!(project.errors[:limit_reached], 403) end + render_validation_error!(project) end end diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb index f9a47101e27..5b54734bb45 100644 --- a/lib/api/v3/repositories.rb +++ b/lib/api/v3/repositories.rb @@ -14,6 +14,7 @@ module API if errors[:project_access].any? error!(errors[:project_access], 422) end + not_found! end end diff --git a/lib/api/v3/snippets.rb b/lib/api/v3/snippets.rb index 126ec72248e..85613c8ed84 100644 --- a/lib/api/v3/snippets.rb +++ b/lib/api/v3/snippets.rb @@ -97,6 +97,7 @@ module API attrs = declared_params(include_missing: false) UpdateSnippetService.new(nil, current_user, snippet, attrs).execute + if snippet.persisted? present snippet, with: ::API::Entities::PersonalSnippet else diff --git a/lib/backup/database.rb b/lib/backup/database.rb index d97e5d98229..5e6828de597 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -31,6 +31,7 @@ module Backup pgsql_args << "-n" pgsql_args << Gitlab.config.backup.pg_schema end + spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr) end compress_wr.close diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 2a04c03919d..6715159a1aa 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -47,6 +47,7 @@ module Backup if File.exist?(path_to_wiki_repo) progress.print " * #{display_repo_path(wiki)} ... " + if empty_repo?(wiki) progress.puts " [SKIPPED]".color(:cyan) else diff --git a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb new file mode 100644 index 00000000000..7bffffec94d --- /dev/null +++ b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation +# rubocop:disable Metrics/LineLength + +module Gitlab + module BackgroundMigration + class AddMergeRequestDiffCommitsCount + class MergeRequestDiff < ActiveRecord::Base + self.table_name = 'merge_request_diffs' + end + + def perform(start_id, stop_id) + Rails.logger.info("Setting commits_count for merge request diffs: #{start_id} - #{stop_id}") + + update = ' + commits_count = ( + SELECT count(*) + FROM merge_request_diff_commits + WHERE merge_request_diffs.id = merge_request_diff_commits.merge_request_diff_id + )'.squish + + MergeRequestDiff.where(id: start_id..stop_id).update_all(update) + end + end + end +end diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index e25916528f4..35eadf6fa93 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -148,6 +148,7 @@ module Gitlab stream.seek(@offset) append = @offset > 0 end + start_offset = @offset open_new_tag @@ -155,6 +156,7 @@ module Gitlab stream.each_line do |line| s = StringScanner.new(line) until s.eos? + if s.scan(Gitlab::Regex.build_trace_section_regex) handle_section(s) elsif s.scan(/\e([@-_])(.*?)([@-~])/) @@ -168,6 +170,7 @@ module Gitlab else @out << s.scan(/./m) end + @offset += s.matched_size end end @@ -236,8 +239,10 @@ module Gitlab if @style_mask & STYLE_SWITCHES[:bold] != 0 fg_color.sub!(/fg-([a-z]{2,}+)/, 'fg-l-\1') end + css_classes << fg_color end + css_classes << @bg_color unless @bg_color.nil? STYLE_SWITCHES.each do |css_class, flag| diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb index dcbdf9a64b0..8b3bc3e440d 100644 --- a/lib/gitlab/cycle_analytics/base_query.rb +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -15,7 +15,6 @@ module Gitlab query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) .where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables - .where(issue_table[:deleted_at].eq(nil)) .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables # Load merge_requests diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 7b35c24d153..592a1956ceb 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -512,6 +512,7 @@ module Gitlab batch_size: 10_000, interval: 10.minutes ) + unless relation.model < EachBatch raise TypeError, 'The relation must include the EachBatch module' end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb index d32616862f0..979225dd216 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb @@ -26,6 +26,7 @@ module Gitlab move_repository(project, old_full_path, new_full_path) move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki") end + move_uploads(old_full_path, new_full_path) unless project.hashed_storage?(:attachments) move_pages(old_full_path, new_full_path) end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index b669ee5b799..0f897e6316c 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -14,6 +14,7 @@ module Gitlab else @diff_lines = diff_lines end + @raw_lines = @diff_lines.map(&:text) end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 37face8e7d0..d3b49b1ec75 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -156,12 +156,14 @@ module Gitlab %W[git apply --3way #{patch_path}] ) do |output, status| puts output + unless status.zero? @failed_files = output.lines.reduce([]) do |memo, line| if line.start_with?('error: patch failed:') file = line.sub(/\Aerror: patch failed: /, '') memo << file unless file =~ IGNORED_FILES_REGEX end + memo end diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb index e2f7c1d0257..3436306e122 100644 --- a/lib/gitlab/email/handler/create_merge_request_handler.rb +++ b/lib/gitlab/email/handler/create_merge_request_handler.rb @@ -10,6 +10,7 @@ module Gitlab def initialize(mail, mail_key) super(mail, mail_key) + if m = /\A([^\+]*)\+merge-request\+(.*)/.match(mail_key.to_s) @project_path, @incoming_email_token = m.captures end diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 5e426b13ade..8953bc8c148 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -112,6 +112,7 @@ module Gitlab [bug['sCategory'], bug['sPriority']].each do |label| unless label.blank? labels << label + unless @known_labels.include?(label) create_label(label) @known_labels << label @@ -265,6 +266,7 @@ module Gitlab if content.blank? content = '*(No description has been entered for this issue)*' end + body << content body.join("\n\n") @@ -278,6 +280,7 @@ module Gitlab if content.blank? content = "*(No comment has been entered for this change)*" end + body << content if updates.any? diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fa9bc57dd79..6ada9a145db 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -571,7 +571,21 @@ module Gitlab end def merged_branch_names(branch_names = []) - Set.new(git_merged_branch_names(branch_names)) + return [] unless root_ref + + root_sha = find_branch(root_ref)&.target + + return [] unless root_sha + + branches = gitaly_migrate(:merged_branch_names) do |is_enabled| + if is_enabled + gitaly_merged_branch_names(branch_names, root_sha) + else + git_merged_branch_names(branch_names, root_sha) + end + end + + Set.new(branches) end # Return an array of Diff objects that represent the diff @@ -654,6 +668,7 @@ module Gitlab end end end + @refs_hash end @@ -1488,14 +1503,7 @@ module Gitlab sort_branches(branches, sort_by) end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/695 - def git_merged_branch_names(branch_names = []) - return [] unless root_ref - - root_sha = find_branch(root_ref)&.target - - return [] unless root_sha - + def git_merged_branch_names(branch_names, root_sha) git_arguments = %W[branch --merged #{root_sha} --format=%(refname:short)\ %(objectname)] + branch_names @@ -1509,6 +1517,14 @@ module Gitlab end end + def gitaly_merged_branch_names(branch_names, root_sha) + qualified_branch_names = branch_names.map { |b| "refs/heads/#{b}" } + + gitaly_ref_client.merged_branches(qualified_branch_names) + .reject { |b| b.target == root_sha } + .map(&:name) + end + def process_count_commits_options(options) if options[:from] || options[:to] ref = diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb index 1307f400700..0a4e557b59b 100644 --- a/lib/gitlab/git/storage/forked_storage_check.rb +++ b/lib/gitlab/git/storage/forked_storage_check.rb @@ -27,6 +27,7 @@ module Gitlab status = nil while status.nil? + if deadline > Time.now.utc sleep(wait_time) _pid, status = Process.wait2(filesystem_check_pid, Process::WNOHANG) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index ae1753ff0ae..37ba1af8d6f 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -52,6 +52,7 @@ module Gitlab ) response = GitalyClient.call(@repository.storage, :operation_service, :user_create_branch, request) + if response.pre_receive_error.present? raise Gitlab::Git::HooksService::PreReceiveError.new(response.pre_receive_error) end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 5bce1009878..f8e2a27f3fe 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -14,12 +14,18 @@ module Gitlab request = Gitaly::FindAllBranchesRequest.new(repository: @gitaly_repo) response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request) - response.flat_map do |message| - message.branches.map do |branch| - target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target) - Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit) - end - end + consume_find_all_branches_response(response) + end + + def merged_branches(branch_names = []) + request = Gitaly::FindAllBranchesRequest.new( + repository: @gitaly_repo, + merged_only: true, + merged_branches: branch_names.map { |s| encode_binary(s) } + ) + response = GitalyClient.call(@storage, :ref_service, :find_all_branches, request) + + consume_find_all_branches_response(response) end def default_branch_name @@ -62,7 +68,7 @@ module Gitlab request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request.sort_by = sort_by_param(sort_by) if sort_by response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request) - consume_branches_response(response) + consume_find_local_branches_response(response) end def tags @@ -151,7 +157,7 @@ module Gitlab enum_value end - def consume_branches_response(response) + def consume_find_local_branches_response(response) response.flat_map do |message| message.branches.map do |gitaly_branch| Gitlab::Git::Branch.new( @@ -164,6 +170,15 @@ module Gitlab end end + def consume_find_all_branches_response(response) + response.flat_map do |message| + message.branches.map do |branch| + target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target) + Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit) + end + end + end + def consume_tags_response(response) response.flat_map do |message| message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) } diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index ab38c0c3e34..46b49128140 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -302,6 +302,7 @@ module Gitlab else "#{project.namespace.full_path}/#{name}##{id}" end + text = "~~#{text}~~" if deleted text end @@ -329,6 +330,7 @@ module Gitlab if content.blank? content = "*(No comment has been entered for this change)*" end + body << content if updates.any? @@ -352,6 +354,7 @@ module Gitlab if content.blank? content = "*(No description has been entered for this issue)*" end + body << content if attachments.any? diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index e29dd0d5b0e..f9b1a3caf5e 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -7,7 +7,6 @@ module Gitlab closed_at confidential created_at - deleted_at description due_date id diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index ae9b68eb648..aff786864f2 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -5,7 +5,6 @@ module Gitlab assignee_id author_id created_at - deleted_at description head_pipeline_id id diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index c518943be59..4b5f9f3a926 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -148,6 +148,7 @@ module Gitlab else relation_hash = relation_item[sub_relation.to_s] end + [relation_hash, sub_relation] end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 05dbaf6322c..cb711a83433 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -267,6 +267,7 @@ module Gitlab else %w[title group_id] end + finder_hash = parsed_relation_hash.slice(*finder_attributes) if label? diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index 233f6bf6227..97ad3c97e95 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -14,6 +14,7 @@ module Gitlab generate_config_map spec['volumes'] = volumes_specification end + ::Kubeclient::Resource.new(metadata: metadata, spec: spec) end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index 0d9a554fc18..cde60addcf7 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -42,6 +42,7 @@ module Gitlab else self.class.invalid_provider(provider) end + @options = config_for(@provider) # Use @provider, not provider end diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb index 877cebf6786..ef44a13df51 100644 --- a/lib/gitlab/metrics/influx_db.rb +++ b/lib/gitlab/metrics/influx_db.rb @@ -169,6 +169,7 @@ module Gitlab end end end + @pool end end diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index fee741b47be..cc1e92480be 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -47,6 +47,7 @@ module Gitlab else value = decorate_params_value(value, @request.params[key], tmp_path) end + @request.update_param(key, value) end @@ -60,6 +61,7 @@ module Gitlab unless path_hash.is_a?(Hash) && path_hash.count == 1 raise "invalid path: #{path_hash.inspect}" end + path_key, path_value = path_hash.first unless value_hash.is_a?(Hash) && value_hash[path_key] diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb index c22d0a84860..43921a8c1c0 100644 --- a/lib/gitlab/multi_collection_paginator.rb +++ b/lib/gitlab/multi_collection_paginator.rb @@ -37,6 +37,7 @@ module Gitlab else per_page - first_collection_last_page_size end + hash[page] = second_collection.page(second_collection_page) .per(per_page - paginated_first_collection(page).size) .padding(offset) diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index 3ebfa3bd4b8..c0878a34fb1 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -126,6 +126,7 @@ module Gitlab command << match_data[1] unless match_data[1].empty? commands << command end + content = substitution.perform_substitution(self, content) end diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb index 8ad06480575..4178b436acf 100644 --- a/lib/gitlab/redis/wrapper.rb +++ b/lib/gitlab/redis/wrapper.rb @@ -24,6 +24,7 @@ module Gitlab # the pool will be used in a multi-threaded context size += Sidekiq.options[:concurrency] end + size end @@ -104,6 +105,7 @@ module Gitlab db_numbers = queries["db"] if queries.key?("db") config[:db] = db_numbers[0].to_i if db_numbers.any? end + config else redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_url) diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index ca48c6df602..70b639501fd 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -115,6 +115,7 @@ module Gitlab else merge_requests.full_search(query) end + merge_requests.order('updated_at DESC') end diff --git a/lib/gitlab/storage_check/cli.rb b/lib/gitlab/storage_check/cli.rb index 04bf1bf1d26..9b64c8e033a 100644 --- a/lib/gitlab/storage_check/cli.rb +++ b/lib/gitlab/storage_check/cli.rb @@ -59,9 +59,11 @@ module Gitlab if response.skipped_shards.any? warnings << "Skipped shards: #{response.skipped_shards.join(', ')}" end + if response.failing_shards.any? warnings << "Failing shards: #{response.failing_shards.join(', ')}" end + logger.warn(warnings.join(' - ')) if warnings.any? end end diff --git a/lib/gitlab/testing/request_blocker_middleware.rb b/lib/gitlab/testing/request_blocker_middleware.rb index 4a8e3c2eee0..53333b9b06b 100644 --- a/lib/gitlab/testing/request_blocker_middleware.rb +++ b/lib/gitlab/testing/request_blocker_middleware.rb @@ -37,12 +37,14 @@ module Gitlab def call(env) increment_active_requests + if block_requests? block_request(env) else sleep 0.2 if slow_requests? @app.call(env) end + ensure decrement_active_requests end diff --git a/lib/gitlab/timeless.rb b/lib/gitlab/timeless.rb index b290c716f97..76a1808c8ac 100644 --- a/lib/gitlab/timeless.rb +++ b/lib/gitlab/timeless.rb @@ -9,6 +9,7 @@ module Gitlab else block.call end + ensure model.record_timestamps = original_record_timestamps end diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb index 961df0468a4..3b64cb32afa 100644 --- a/lib/gitlab/upgrader.rb +++ b/lib/gitlab/upgrader.rb @@ -12,6 +12,7 @@ module Gitlab puts "You are using the latest GitLab version" else puts "Newer GitLab version is available" + answer = if ARGV.first == "-y" "yes" else @@ -77,6 +78,7 @@ module Gitlab update_commands.each do |title, cmd| puts title puts " -> #{cmd.join(' ')}" + if system(env, *cmd) puts " -> OK" else diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 5ab6cd5a4ef..0de183858aa 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -42,6 +42,7 @@ module Gitlab else raise "Unsupported action: #{action}" end + if feature_enabled params[:GitalyServer] = server end @@ -97,6 +98,9 @@ module Gitlab ) end + # If present DisableCache must be a Boolean. Otherwise workhorse ignores it. + params['DisableCache'] = true if git_archive_cache_disabled? + [ SEND_DATA_HEADER, "git-archive:#{encode(params)}" @@ -244,6 +248,10 @@ module Gitlab right_commit_id: diff_refs.head_sha } end + + def git_archive_cache_disabled? + ENV['WORKHORSE_ARCHIVE_CACHE_DISABLED'].present? || Feature.enabled?(:workhorse_archive_cache_disabled) + end end end end diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb index f05d001fd02..ff638c07755 100644 --- a/lib/google_api/cloud_platform/client.rb +++ b/lib/google_api/cloud_platform/client.rb @@ -47,15 +47,15 @@ module GoogleApi service.authorization = access_token service.fetch_all(items: :projects) do |token| - service.list_projects(page_token: token) + service.list_projects(page_token: token, options: user_agent_header) end end - def projects_get_billing_info(project_name) + def projects_get_billing_info(project_id) service = Google::Apis::CloudbillingV1::CloudbillingService.new service.authorization = access_token - service.get_project_billing_info("projects/#{project_name}") + service.get_project_billing_info("projects/#{project_id}", options: user_agent_header) end def projects_zones_clusters_get(project_id, zone, cluster_id) diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index 8b145fb4511..d268f501b4a 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -66,6 +66,7 @@ module SystemCheck if check.can_repair? $stdout.print 'Trying to fix error automatically. ...' + if check.repair! $stdout.puts 'Success'.color(:green) return diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 9dcf44fdc3e..2383bcf954b 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -46,6 +46,7 @@ namespace :gitlab do puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) sleep(5) end + # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade $progress.puts 'Cleaning the database ... '.color(:blue) @@ -222,6 +223,7 @@ namespace :gitlab do task restore: :environment do $progress.puts "Restoring container registry images ... ".color(:blue) + if Gitlab.config.registry.enabled Backup::Registry.new.restore $progress.puts "done".color(:green) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 903e84359cd..31cd6bfe6e1 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -180,6 +180,7 @@ namespace :gitlab do puts "can't check, you have no projects".color(:magenta) return end + puts "" Project.find_each(batch_size: 100) do |project| @@ -210,6 +211,7 @@ namespace :gitlab do gitlab_shell_repo_base = gitlab_shell_path check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) puts "Running #{check_cmd}" + if system(check_cmd, chdir: gitlab_shell_repo_base) puts 'gitlab-shell self-check successful'.color(:green) else @@ -285,6 +287,7 @@ namespace :gitlab do return if process_count.zero? print 'Number of Sidekiq processes ... ' + if process_count == 1 puts '1'.color(:green) else diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index eb0f757aea7..04d56509ac6 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -84,6 +84,7 @@ namespace :gitlab do next unless user.ldap_user? print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." + if Gitlab::LDAP::Access.allowed?(user) puts " [OK]".color(:green) else diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake index ba221e44e5d..77c28615856 100644 --- a/lib/tasks/gitlab/dev.rake +++ b/lib/tasks/gitlab/dev.rake @@ -14,6 +14,7 @@ namespace :gitlab do puts "Must specify a branch as an argument".color(:red) exit 1 end + args end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 4507b841964..a2e68c0471b 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -5,9 +5,11 @@ namespace :gitlab do require 'toml' warn_user_is_not_gitlab + unless args.dir.present? abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") end + args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitaly.git') version = Gitlab::GitalyClient.expected_server_version diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake index b732db9db6e..d7f28691098 100644 --- a/lib/tasks/gitlab/list_repos.rake +++ b/lib/tasks/gitlab/list_repos.rake @@ -8,6 +8,7 @@ namespace :gitlab do namespace_ids = Namespace.where(['updated_at > ?', date]).pluck(:id).sort scope = scope.where('id IN (?) OR namespace_id in (?)', project_ids, namespace_ids) end + scope.find_each do |project| base = File.join(project.repository_storage_path, project.disk_path) puts base + '.git' diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index f44abc2b81b..a25f7ce59c7 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -10,6 +10,7 @@ namespace :gitlab do puts "This rake task is not meant fo production instances".red exit(1) end + admin = User.find_by(admin: true) unless admin diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake index e7ac0b5859f..308ffb0e284 100644 --- a/lib/tasks/gitlab/workhorse.rake +++ b/lib/tasks/gitlab/workhorse.rake @@ -3,9 +3,11 @@ namespace :gitlab do desc "GitLab | Install or upgrade gitlab-workhorse" task :install, [:dir, :repo] => :environment do |t, args| warn_user_is_not_gitlab + unless args.dir.present? abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]") end + args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-workhorse.git') version = Gitlab::Workhorse.version diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake index fc2cea8c016..aa2d01730d7 100644 --- a/lib/tasks/migrate/migrate_iids.rake +++ b/lib/tasks/migrate/migrate_iids.rake @@ -4,6 +4,7 @@ task migrate_iids: :environment do Issue.where(iid: nil).find_each(batch_size: 100) do |issue| begin issue.set_iid + if issue.update_attribute(:iid, issue.iid) print '.' else @@ -19,6 +20,7 @@ task migrate_iids: :environment do MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| begin mr.set_iid + if mr.update_attribute(:iid, mr.iid) print '.' else @@ -34,6 +36,7 @@ task migrate_iids: :environment do Milestone.where(iid: nil).find_each(batch_size: 100) do |m| begin m.set_iid + if m.update_attribute(:iid, m.iid) print '.' else diff --git a/qa/README.md b/qa/README.md index 7f2dd39ff63..8fa04e80825 100644 --- a/qa/README.md +++ b/qa/README.md @@ -27,13 +27,17 @@ following call would login to a local [GDK] instance and run all specs in bin/qa Test::Instance http://localhost:3000 ``` +### Writing tests + +1. [Using page objects](qa/page/README.md) + ### Running specific tests You can also supply specific tests to run as another parameter. For example, to -test the EE license specs, you can run: +run the repository-related specs, you can execute: ``` -EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/specs/features/ee +bin/qa Test::Instance http://localhost qa/specs/features/repository/ ``` Since the arguments would be passed to `rspec`, you could use all `rspec` @@ -58,6 +58,10 @@ module QA module Integration autoload :Mattermost, 'qa/scenario/test/integration/mattermost' end + + module Sanity + autoload :Selectors, 'qa/scenario/test/sanity/selectors' + end end end @@ -68,6 +72,9 @@ module QA # module Page autoload :Base, 'qa/page/base' + autoload :View, 'qa/page/view' + autoload :Element, 'qa/page/element' + autoload :Validator, 'qa/page/validator' module Main autoload :Login, 'qa/page/main/login' diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index 00851a7bece..bd66b74a164 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -1,12 +1,19 @@ +require 'forwardable' + module QA module Factory class Base + extend SingleForwardable + + def_delegators :evaluator, :dependency, :dependencies + def_delegators :evaluator, :product, :attributes + def fabricate!(*_args) raise NotImplementedError end def self.fabricate!(*args) - Factory::Product.populate!(new) do |factory| + new.tap do |factory| yield factory if block_given? dependencies.each do |name, signature| @@ -14,19 +21,37 @@ module QA end factory.fabricate!(*args) + + return Factory::Product.populate!(self) end end - def self.dependencies - @dependencies ||= {} + def self.evaluator + @evaluator ||= Factory::Base::DSL.new(self) end - def self.dependency(factory, as:, &block) - as.tap do |name| - class_eval { attr_accessor name } + class DSL + attr_reader :dependencies, :attributes + + def initialize(base) + @base = base + @dependencies = {} + @attributes = {} + end + + def dependency(factory, as:, &block) + as.tap do |name| + @base.class_eval { attr_accessor name } + + Dependency::Signature.new(factory, block).tap do |signature| + @dependencies.store(name, signature) + end + end + end - Dependency::Signature.new(factory, block).tap do |signature| - dependencies.store(name, signature) + def product(attribute, &block) + Product::Attribute.new(attribute, block).tap do |signature| + @attributes.store(attribute, signature) end end end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb index df35bbbb443..d004e642f9b 100644 --- a/qa/qa/factory/product.rb +++ b/qa/qa/factory/product.rb @@ -5,8 +5,9 @@ module QA class Product include Capybara::DSL - def initialize(factory) - @factory = factory + Attribute = Struct.new(:name, :block) + + def initialize @location = current_url end @@ -15,11 +16,13 @@ module QA end def self.populate!(factory) - raise ArgumentError unless block_given? - - yield factory - - new(factory) + new.tap do |product| + factory.attributes.each_value do |attribute| + product.instance_exec(&attribute.block).tap do |value| + product.define_singleton_method(attribute.name) { value } + end + end + end end end end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb index 07c2e3086d1..7df2dc6618c 100644 --- a/qa/qa/factory/resource/project.rb +++ b/qa/qa/factory/resource/project.rb @@ -13,6 +13,10 @@ module QA @description = 'My awesome project' end + product :name do + Page::Project::Show.act { project_name } + end + def fabricate! group.visit! diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md new file mode 100644 index 00000000000..f72fbfeafca --- /dev/null +++ b/qa/qa/page/README.md @@ -0,0 +1,112 @@ +# Page objects in GitLab QA + +In GitLab QA we are using a known pattern, called _Page Objects_. + +This means that we have built an abstraction for all GitLab pages that we use +to drive GitLab QA scenarios. Whenever we do something on a page, like filling +in a form, or clicking a button, we do that only through a page object +associated with this area of GitLab. + +For example, when GitLab QA test harness signs in into GitLab, it needs to fill +in a user login and user password. In order to do that, we have a class, called +`Page::Main::Login` and `sign_in_using_credentials` methods, that is the only +piece of the code, that has knowledge about `user_login` and `user_password` +fields. + +## Why do we need that? + +We need page objects, because we need to reduce duplication and avoid problems +whenever someone changes some selectors in GitLab's source code. + +Imagine that we have a hundred specs in GitLab QA, and we need to sign into +GitLab each time, before we make assertions. Without a page object one would +need to rely on volatile helpers or invoke Capybara methods directly. Imagine +invoking `fill_in :user_login` in every `*_spec.rb` file / test example. + +When someone later changes `t.text_field :login` in the view associated with +this page to `t.text_field :username` it will generate a different field +identifier, what would effectively break all tests. + +Because we are using `Page::Main::Login.act { sign_in_using_credentials }` +everywhere, when we want to sign into GitLab, the page object is the single +source of truth, and we will need to update `fill_in :user_login` +to `fill_in :user_username` only in a one place. + +## What problems did we have in the past? + +We do not run QA tests for every commit, because of performance reasons, and +the time it would take to build packages and test everything. + +That is why when someone changes `t.text_field :login` to +`t.text_field :username` in the _new session_ view we won't know about this +change until our GitLab QA nightly pipeline fails, or until someone triggers +`package-qa` action in their merge request. + +Obviously such a change would break all tests. We call this problem a _fragile +tests problem_. + +In order to make GitLab QA more reliable and robust, we had to solve this +problem by introducing coupling between GitLab CE / EE views and GitLab QA. + +## How did we solve fragile tests problem? + +Currently, when you add a new `Page::Base` derived class, you will also need to +define all selectors that your page objects depends on. + +Whenever you push your code to CE / EE repository, `qa:selectors` sanity test +job is going to be run as a part of a CI pipeline. + +This test is going to validate all page objects that we have implemented in +`qa/page` directory. When it fails, you will be notified about missing +or invalid views / selectors definition. + +## How to properly implement a page object? + +We have built a DSL to define coupling between a page object and GitLab views +it is actually implemented by. See an example below. + +```ruby +module Page + module Main + class Login < Page::Base + view 'app/views/devise/passwords/edit.html.haml' do + element :password_field, 'password_field :password' + element :password_confirmation, 'password_field :password_confirmation' + element :change_password_button, 'submit "Change your password"' + end + + view 'app/views/devise/sessions/_new_base.html.haml' do + element :login_field, 'text_field :login' + element :passowrd_field, 'password_field :password' + element :sign_in_button, 'submit "Sign in"' + end + + # ... + end +end +``` + +It is possible to use `element` DSL method without value, with a String value +or with a Regexp. + +```ruby +view 'app/views/my/view.html.haml' do + # Require `f.submit "Sign in"` to be present in `my/view.html.haml + element :my_button, 'f.submit "Sign in"' + + # Match every line in `my/view.html.haml` against + # `/link_to .* "My Profile"/` regexp. + element :profile_link, /link_to .* "My Profile"/ + + # Implicitly require `.qa-logout-button` CSS class to be present in the view + element :logout_button +end +``` + +## Where to ask for help? + +If you need more information, ask for help on `#qa` channel on Slack (GitLab +Team only). + +If you are not a Team Member, and you still need help to contribute, please +open an issue in GitLab QA issue tracker. diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb index 39e2f2062ad..1904732aee6 100644 --- a/qa/qa/page/admin/settings.rb +++ b/qa/qa/page/admin/settings.rb @@ -2,6 +2,13 @@ module QA module Page module Admin class Settings < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/admin/application_settings/show.html.haml' + def enable_hashed_storage scroll_to 'legend', text: 'Repository Storage' check 'Create new projects using hashed storage paths' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 99eba02b6e3..ea4c920c82c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -5,6 +5,9 @@ module QA class Base include Capybara::DSL include Scenario::Actable + extend SingleForwardable + + def_delegators :evaluator, :view, :views def refresh visit current_url @@ -37,9 +40,39 @@ module QA page.within(selector) { yield } if block_given? end + def click_element(name) + find(Page::Element.new(name).selector_css).click + end + def self.path raise NotImplementedError end + + def self.evaluator + @evaluator ||= Page::Base::DSL.new + end + + def self.errors + if views.empty? + return ["Page class does not have views / elements defined!"] + end + + views.map(&:errors).flatten + end + + class DSL + attr_reader :views + + def initialize + @views = [] + end + + def view(path, &block) + Page::View.evaluate(&block).tap do |view| + @views.push(Page::View.new(path, view.elements)) + end + end + end end end end diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb index 083d2e1ab16..e853e0d85e0 100644 --- a/qa/qa/page/dashboard/groups.rb +++ b/qa/qa/page/dashboard/groups.rb @@ -2,6 +2,15 @@ module QA module Page module Dashboard class Groups < Page::Base + view 'app/views/shared/groups/_search_form.html.haml' do + element :groups_filter, 'search_field_tag :filter' + element :groups_filter_placeholder, 'Filter by name...' + end + + view 'app/views/dashboard/_groups_head.html.haml' do + element :new_group_button, 'link_to _("New group")' + end + def filter_by_name(name) fill_in 'Filter by name...', with: name end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 7ed27da6d89..71255b18362 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -2,6 +2,8 @@ module QA module Page module Dashboard class Projects < Page::Base + view 'app/views/dashboard/projects/index.html.haml' + def go_to_project(name) find_link(text: name).click end diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb new file mode 100644 index 00000000000..9944a39ce07 --- /dev/null +++ b/qa/qa/page/element.rb @@ -0,0 +1,32 @@ +module QA + module Page + class Element + attr_reader :name + + def initialize(name, pattern = nil) + @name = name + @pattern = pattern || selector + end + + def selector + "qa-#{@name.to_s.tr('_', '-')}" + end + + def selector_css + ".#{selector}" + end + + def expression + if @pattern.is_a?(String) + @_regexp ||= Regexp.new(Regexp.escape(@pattern)) + else + @pattern + end + end + + def matches?(line) + !!(line =~ expression) + end + end + end +end diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index 53fdaaed078..48b71a7c883 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -2,6 +2,17 @@ module QA module Page module Group class New < Page::Base + view 'app/views/shared/_group_form.html.haml' do + element :group_path_field, 'text_field :path' + element :group_name_field, 'text_field :name' + element :group_description_field, 'text_area :description' + end + + view 'app/views/groups/new.html.haml' do + element :create_group_button, "submit 'Create group'" + element :visibility_radios, 'visibility_level:' + end + def set_path(path) fill_in 'group_path', with: path fill_in 'group_name', with: path diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 0a16c07d64b..37ed3b35bce 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -2,6 +2,13 @@ module QA module Page module Group class Show < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/groups/show.html.haml' + def go_to_subgroup(name) click_link name end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index f88325f408b..7b4c1603017 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -2,6 +2,18 @@ module QA module Page module Main class Login < Page::Base + view 'app/views/devise/passwords/edit.html.haml' do + element :password_field, 'password_field :password' + element :password_confirmation, 'password_field :password_confirmation' + element :change_password_button, 'submit "Change your password"' + end + + view 'app/views/devise/sessions/_new_base.html.haml' do + element :login_field, 'text_field :login' + element :passowrd_field, 'password_field :password' + element :sign_in_button, 'submit "Sign in"' + end + def initialize wait('.application', time: 500) end diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb index e746cff0a80..6f548148363 100644 --- a/qa/qa/page/main/oauth.rb +++ b/qa/qa/page/main/oauth.rb @@ -2,6 +2,10 @@ module QA module Page module Main class OAuth < Page::Base + view 'app/views/doorkeeper/authorizations/new.html.haml' do + element :authorization_button, 'submit_tag "Authorize"' + end + def needs_authorization? page.current_url.include?('/oauth') end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 8ffd4fdad13..9b21300ea3c 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -2,6 +2,13 @@ module QA module Page module Mattermost class Login < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/mattermosts/new.html.haml' + def sign_in_using_oauth click_link class: 'btn btn-custom-login gitlab' diff --git a/qa/qa/page/mattermost/main.rb b/qa/qa/page/mattermost/main.rb index 4b8fc28e53f..bc2f9acc729 100644 --- a/qa/qa/page/mattermost/main.rb +++ b/qa/qa/page/mattermost/main.rb @@ -2,6 +2,13 @@ module QA module Page module Mattermost class Main < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/mattermosts/new.html.haml' + def initialize visit(Runtime::Scenario.mattermost_address) end diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/menu/admin.rb index 07fe40fda3a..40da4a53e8a 100644 --- a/qa/qa/page/menu/admin.rb +++ b/qa/qa/page/menu/admin.rb @@ -2,6 +2,13 @@ module QA module Page module Menu class Admin < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/admin/dashboard/index.html.haml' + def go_to_license click_link 'License' end diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb index b94c2c6c23d..f8978b8a5f7 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/menu/main.rb @@ -2,19 +2,40 @@ module QA module Page module Menu class Main < Page::Base + view 'app/views/layouts/header/_default.html.haml' do + element :navbar + element :user_avatar + element :user_menu, '.dropdown-menu-nav' + element :user_sign_out_link, 'link_to "Sign out"' + end + + view 'app/views/layouts/nav/_dashboard.html.haml' do + element :admin_area_link + element :projects_dropdown + element :groups_link + end + + view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do + element :projects_dropdown_sidebar + element :your_projects_link + end + def go_to_groups - within_top_menu { click_link 'Groups' } + within_top_menu { click_element :groups_link } end def go_to_projects within_top_menu do - click_link 'Projects' - click_link 'Your projects' + click_element :projects_dropdown + end + + page.within('.qa-projects-dropdown-sidebar') do + click_element :your_projects_link end end def go_to_admin_area - within_top_menu { find('.admin-icon').click } + within_top_menu { click_element :admin_area_link } end def sign_out @@ -24,20 +45,20 @@ module QA end def has_personal_area? - page.has_selector?('.header-user-dropdown-toggle') + page.has_selector?('.qa-user-avatar') end private def within_top_menu - page.within('.navbar') do + page.within('.qa-navbar') do yield end end def within_user_menu within_top_menu do - find('.header-user-dropdown-toggle').click + click_element :user_avatar page.within('.dropdown-menu-nav') do yield diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb index 6c25aba4bac..1df4e0c2429 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/menu/side.rb @@ -2,6 +2,12 @@ module QA module Page module Menu class Side < Page::Base + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :settings_item + element :repository_link, "title: 'Repository'" + element :top_level_items, '.sidebar-top-level-items' + end + def click_repository_setting hover_setting do click_link('Repository') @@ -12,7 +18,7 @@ module QA def hover_setting within_sidebar do - find('.nav-item-name', text: 'Settings').hover + find('.qa-settings-item').hover yield end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index b31bec27b59..9b1438f76d5 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -2,9 +2,18 @@ module QA module Page module Project class New < Page::Base + view 'app/views/projects/_new_project_fields.html.haml' do + element :project_namespace_select + element :project_namespace_field, 'select :namespace_id' + element :project_path, 'text_field :path' + element :project_description, 'text_area :description' + element :project_create_button, "submit 'Create project'" + end + def choose_test_namespace - find('#s2id_project_namespace_id').click - find('.select2-result-label', text: Runtime::Namespace.name).click + click_element :project_namespace_select + + first('li', text: Runtime::Namespace.path).click end def choose_name(name) diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 4028b8cccc5..a8d6f09777c 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -3,6 +3,13 @@ module QA module Project module Settings class DeployKeys < Page::Base + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/deploy_keys/edit.html.haml' + def fill_key_title(title) fill_in 'deploy_key_title', with: title end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 034b0d09c1c..524d87c6be9 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -5,6 +5,13 @@ module QA class Repository < Page::Base include Common + ## + # TODO, define all selectors required by this page object + # + # See gitlab-org/gitlab-qa#154 + # + view 'app/views/projects/settings/repository/show.html.haml' + def expand_deploy_keys(&block) expand('.qa-expand-deploy-keys') do DeployKeys.perform(&block) diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 3b2bac84f3f..c8af5ba6280 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -2,8 +2,21 @@ module QA module Page module Project class Show < Page::Base + view 'app/views/shared/_clone_panel.html.haml' do + element :clone_dropdown + element :clone_options_dropdown, '.clone-options-dropdown' + end + + view 'app/views/shared/_clone_panel.html.haml' do + element :project_repository_location, 'text_field_tag :project_clone' + end + + view 'app/views/projects/_home_panel.html.haml' do + element :project_name + end + def choose_repository_clone_http - find('#clone-dropdown').click + click_element :clone_dropdown page.within('.clone-options-dropdown') do click_link('HTTP') @@ -15,7 +28,7 @@ module QA end def project_name - find('.project-title').text + find('.qa-project-name').text end def wait_for_push diff --git a/qa/qa/page/validator.rb b/qa/qa/page/validator.rb new file mode 100644 index 00000000000..117d8d4db67 --- /dev/null +++ b/qa/qa/page/validator.rb @@ -0,0 +1,52 @@ +module QA + module Page + class Validator + ValidationError = Class.new(StandardError) + + Error = Struct.new(:page, :message) do + def to_s + "Error: #{page} - #{message}" + end + end + + def initialize(constant) + @module = constant + end + + def constants + @consts ||= @module.constants.map do |const| + @module.const_get(const) + end + end + + def descendants + @descendants ||= constants.map do |const| + case const + when Class + const if const < Page::Base + when Module + Page::Validator.new(const).descendants + end + end + + @descendants.flatten.compact + end + + def errors + [].tap do |errors| + descendants.each do |page| + page.errors.each do |message| + errors.push(Error.new(page.name, message)) + end + end + end + end + + def validate! + return if errors.none? + + raise ValidationError, 'Page views / elements validation error!' + end + end + end +end diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb new file mode 100644 index 00000000000..6635e1ce039 --- /dev/null +++ b/qa/qa/page/view.rb @@ -0,0 +1,55 @@ +module QA + module Page + class View + attr_reader :path, :elements + + def initialize(path, elements) + @path = path + @elements = elements + end + + def pathname + @pathname ||= Pathname.new(File.join(__dir__, '../../../', @path)) + .cleanpath.expand_path + end + + def errors + unless pathname.readable? + return ["Missing view partial `#{pathname}`!"] + end + + ## + # Reduce required elements by streaming view and making assertions on + # elements' existence. + # + @missing ||= @elements.dup.tap do |elements| + File.foreach(pathname.to_s) do |line| + elements.reject! { |element| element.matches?(line) } + end + end + + @missing.map do |missing| + "Missing element `#{missing.name}` in `#{pathname}` view partial!" + end + end + + def self.evaluate(&block) + Page::View::DSL.new.tap do |evaluator| + evaluator.instance_exec(&block) if block_given? + end + end + + class DSL + attr_reader :elements + + def initialize + @elements = [] + end + + def element(name, pattern = nil) + @elements.push(Page::Element.new(name, pattern)) + end + end + end + end +end diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb index b00e925986b..a72c2d21898 100644 --- a/qa/qa/runtime/namespace.rb +++ b/qa/qa/runtime/namespace.rb @@ -11,6 +11,10 @@ module QA 'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S') end + def path + "#{sandbox_name}/#{name}" + end + def sandbox_name 'gitlab-qa-sandbox' end diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb new file mode 100644 index 00000000000..c87eb5f3dfb --- /dev/null +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -0,0 +1,54 @@ +module QA + module Scenario + module Test + module Sanity + class Selectors < Scenario::Template + include Scenario::Bootable + + PAGES = [QA::Page].freeze + + def perform(*) + validators = PAGES.map do |pages| + Page::Validator.new(pages) + end + + validators.map(&:errors).flatten.tap do |errors| + break if errors.none? + + warn <<~EOS + GitLab QA sanity selectors validation test detected problems + with your merge request! + + The purpose of this test is to make sure that GitLab QA tests, + that are entirely black-box, click-driven scenarios, do match + pages structure / layout in GitLab CE / EE repositories. + + It looks like you have changed views / pages / selectors, and + these are now out of sync with what we have defined in `qa/` + directory. + + Please update the code in `qa/` directory to make it match + current changes in this merge request. + + For more help see documentation in `qa/page/README.md` file or + ask for help on #qa channel on Slack (GitLab Team only). + + If you are not a Team Member, and you still need help to + contribute, please open an issue in GitLab QA issue tracker. + + Please see errors described below. + + EOS + + warn errors + end + + validators.each(&:validate!) + + puts 'Views / selectors validation passed!' + end + end + end + end + end +end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index 61c19378ae0..b1c07249892 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -4,11 +4,13 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Project.fabricate! do |project| + created_project = Factory::Resource::Project.fabricate! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end + expect(created_project.name).to match /^awesome-project-\h{16}$/ + expect(page).to have_content( /Project \S?awesome-project\S+ was successfully created/ ) diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index a3ba0176819..90dd58e20fd 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -1,8 +1,9 @@ describe QA::Factory::Base do + let(:factory) { spy('factory') } + let(:product) { spy('product') } + describe '.fabricate!' do subject { Class.new(described_class) } - let(:factory) { spy('factory') } - let(:product) { spy('product') } before do allow(QA::Factory::Product).to receive(:new).and_return(product) @@ -59,30 +60,63 @@ describe QA::Factory::Base do it 'defines dependency accessors' do expect(subject.new).to respond_to :mydep, :mydep= end - end - describe 'building dependencies' do - let(:dependency) { double('dependency') } - let(:instance) { spy('instance') } + describe 'dependencies fabrication' do + let(:dependency) { double('dependency') } + let(:instance) { spy('instance') } + + subject do + Class.new(described_class) do + dependency Some::MyDependency, as: :mydep + end + end + + before do + stub_const('Some::MyDependency', dependency) + allow(subject).to receive(:new).and_return(instance) + allow(instance).to receive(:mydep).and_return(nil) + allow(QA::Factory::Product).to receive(:new) + end + + it 'builds all dependencies first' do + expect(dependency).to receive(:fabricate!).once + + subject.fabricate! + end + end + end + + describe '.product' do subject do Class.new(described_class) do - dependency Some::MyDependency, as: :mydep + product :token do + page.do_something_on_page! + 'resulting value' + end end end - before do - stub_const('Some::MyDependency', dependency) - - allow(subject).to receive(:new).and_return(instance) - allow(instance).to receive(:mydep).and_return(nil) - allow(QA::Factory::Product).to receive(:new) + it 'appends new product attribute' do + expect(subject.attributes).to be_one + expect(subject.attributes).to have_key(:token) end - it 'builds all dependencies first' do - expect(dependency).to receive(:fabricate!).once + describe 'populating fabrication product with data' do + let(:page) { spy('page') } + + before do + allow(subject).to receive(:new).and_return(factory) + allow(QA::Factory::Product).to receive(:new).and_return(product) + allow(product).to receive(:page).and_return(page) + end - subject.fabricate! + it 'populates product after fabrication' do + subject.fabricate! + + expect(page).to have_received(:do_something_on_page!) + expect(product.token).to eq 'resulting value' + end end end end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb index 3d9e86a641b..fdfb1ec90cc 100644 --- a/qa/spec/factory/product_spec.rb +++ b/qa/spec/factory/product_spec.rb @@ -3,19 +3,8 @@ describe QA::Factory::Product do let(:product) { spy('product') } describe '.populate!' do - it 'instantiates and yields factory' do - expect(described_class).to receive(:new).with(factory) - - described_class.populate!(factory) do |instance| - instance.something = 'string' - end - - expect(factory).to have_received(:something=).with('string') - end - it 'returns a fabrication product' do - expect(described_class).to receive(:new) - .with(factory).and_return(product) + expect(described_class).to receive(:new).and_return(product) result = described_class.populate!(factory) do |instance| instance.something = 'string' @@ -23,11 +12,6 @@ describe QA::Factory::Product do expect(result).to be product end - - it 'raises unless block given' do - expect { described_class.populate!(factory) } - .to raise_error ArgumentError - end end describe '.visit!' do @@ -37,8 +21,7 @@ describe QA::Factory::Product do allow_any_instance_of(described_class) .to receive(:visit).and_return('visited some url') - expect(described_class.new(factory).visit!) - .to eq 'visited some url' + expect(subject.visit!).to eq 'visited some url' end end end diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb new file mode 100644 index 00000000000..287adf35c46 --- /dev/null +++ b/qa/spec/page/base_spec.rb @@ -0,0 +1,63 @@ +describe QA::Page::Base do + describe 'page helpers' do + it 'exposes helpful page helpers' do + expect(subject).to respond_to :refresh, :wait, :scroll_to + end + end + + describe '.view', 'DSL for defining view partials' do + subject do + Class.new(described_class) do + view 'path/to/some/view.html.haml' do + element :something, 'string pattern' + element :something_else, /regexp pattern/ + end + + view 'path/to/some/_partial.html.haml' do + element :something, 'string pattern' + end + end + end + + it 'makes it possible to define page views' do + expect(subject.views.size).to eq 2 + expect(subject.views).to all(be_an_instance_of QA::Page::View) + end + + it 'populates views objects with data about elements' do + subject.views.first.elements.tap do |elements| + expect(elements.size).to eq 2 + expect(elements).to all(be_an_instance_of QA::Page::Element) + expect(elements.map(&:name)).to eq [:something, :something_else] + end + end + end + + describe '.errors' do + let(:view) { double('view') } + + context 'when page has views and elements defined' do + before do + allow(described_class).to receive(:views) + .and_return([view]) + + allow(view).to receive(:errors).and_return(['some error']) + end + + it 'iterates views composite and returns errors' do + expect(described_class.errors).to eq ['some error'] + end + end + + context 'when page has no views and elements defined' do + before do + allow(described_class).to receive(:views).and_return([]) + end + + it 'appends an error about missing views / elements block' do + expect(described_class.errors) + .to include 'Page class does not have views / elements defined!' + end + end + end +end diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb new file mode 100644 index 00000000000..8598c57ad34 --- /dev/null +++ b/qa/spec/page/element_spec.rb @@ -0,0 +1,51 @@ +describe QA::Page::Element do + describe '#selector' do + it 'transforms element name into QA-specific selector' do + expect(described_class.new(:sign_in_button).selector) + .to eq 'qa-sign-in-button' + end + end + + describe '#selector_css' do + it 'transforms element name into QA-specific clickable css selector' do + expect(described_class.new(:sign_in_button).selector_css) + .to eq '.qa-sign-in-button' + end + end + + context 'when pattern is an expression' do + subject { described_class.new(:something, /button 'Sign in'/) } + + it 'matches when there is a match' do + expect(subject.matches?("button 'Sign in'")).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?("button 'Sign out'")).to be false + end + end + + context 'when pattern is a string' do + subject { described_class.new(:something, 'button') } + + it 'matches when there is match' do + expect(subject.matches?('some button in the view')).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?('text_field :name')).to be false + end + end + + context 'when pattern is not provided' do + subject { described_class.new(:some_name) } + + it 'matches when QA specific selector is present' do + expect(subject.matches?('some qa-some-name selector')).to be true + end + + it 'does not match if QA selector is not there' do + expect(subject.matches?('some_name selector')).to be false + end + end +end diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb new file mode 100644 index 00000000000..02822d7d18f --- /dev/null +++ b/qa/spec/page/validator_spec.rb @@ -0,0 +1,79 @@ +describe QA::Page::Validator do + describe '#constants' do + subject do + described_class.new(QA::Page::Project) + end + + it 'returns all constants that are module children' do + expect(subject.constants) + .to include QA::Page::Project::New, QA::Page::Project::Settings + end + end + + describe '#descendants' do + subject do + described_class.new(QA::Page::Project) + end + + it 'recursively returns all descendants that are page objects' do + expect(subject.descendants) + .to include QA::Page::Project::New, QA::Page::Project::Settings::Repository + end + + it 'does not return modules that aggregate page objects' do + expect(subject.descendants) + .not_to include QA::Page::Project::Settings + end + end + + context 'when checking validation errors' do + let(:view) { spy('view') } + + before do + allow(QA::Page::Admin::Settings) + .to receive(:views).and_return([view]) + end + + subject do + described_class.new(QA::Page::Admin) + end + + context 'when there are no validation errors' do + before do + allow(view).to receive(:errors).and_return([]) + end + + describe '#errors' do + it 'does not return errors' do + expect(subject.errors).to be_empty + end + end + + describe '#validate!' do + it 'does not raise error' do + expect { subject.validate! }.not_to raise_error + end + end + end + + context 'when there are validation errors' do + before do + allow(view).to receive(:errors) + .and_return(['some error', 'another error']) + end + + describe '#errors' do + it 'returns errors' do + expect(subject.errors.count).to eq 2 + end + end + + describe '#validate!' do + it 'raises validation error' do + expect { subject.validate! } + .to raise_error described_class::ValidationError + end + end + end + end +end diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb new file mode 100644 index 00000000000..aedbc3863a7 --- /dev/null +++ b/qa/spec/page/view_spec.rb @@ -0,0 +1,70 @@ +describe QA::Page::View do + let(:element) do + double('element', name: :something, pattern: /some element/) + end + + subject { described_class.new('some/file.html', [element]) } + + describe '.evaluate' do + it 'evaluates a block and returns a DSL object' do + results = described_class.evaluate do + element :something, 'my pattern' + element :something_else, /another pattern/ + end + + expect(results.elements.size).to eq 2 + end + end + + describe '#pathname' do + it 'returns an absolute and clean path to the view' do + expect(subject.pathname.to_s).not_to include 'qa/page/' + expect(subject.pathname.to_s).to include 'some/file.html' + end + end + + describe '#errors' do + context 'when view partial is present' do + before do + allow(subject.pathname).to receive(:readable?) + .and_return(true) + end + + context 'when pattern is found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(true) + end + + it 'walks through the view and asserts on elements existence' do + expect(subject.errors).to be_empty + end + end + + context 'when pattern has not been found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(false) + end + + it 'returns an array of errors related to missing elements' do + expect(subject.errors).not_to be_empty + expect(subject.errors.first) + .to match %r(Missing element `.*` in `.*/some/file.html` view) + end + end + end + + context 'when view partial has not been found' do + it 'returns an error when it is not able to find the partial' do + expect(subject.errors).to be_one + expect(subject.errors.first) + .to match %r(Missing view partial `.*/some/file.html`!) + end + end + end +end diff --git a/qa/spec/scenario/test/sanity/selectors_spec.rb b/qa/spec/scenario/test/sanity/selectors_spec.rb new file mode 100644 index 00000000000..45d21d54955 --- /dev/null +++ b/qa/spec/scenario/test/sanity/selectors_spec.rb @@ -0,0 +1,40 @@ +describe QA::Scenario::Test::Sanity::Selectors do + let(:validator) { spy('validator') } + + before do + stub_const('QA::Page::Validator', validator) + end + + context 'when there are errors detected' do + before do + allow(validator).to receive(:errors).and_return(['some error']) + end + + it 'outputs information about errors' do + expect { described_class.perform } + .to output(/some error/).to_stderr + + expect { described_class.perform } + .to output(/electors validation test detected problems/) + .to_stderr + end + end + + context 'when there are no errors detected' do + before do + allow(validator).to receive(:errors).and_return([]) + end + + it 'processes pages module' do + described_class.perform + + expect(validator).to have_received(:new).with(QA::Page) + end + + it 'triggers validation' do + described_class.perform + + expect(validator).to have_received(:validate!).at_least(:once) + end + end +end diff --git a/rubocop/cop/line_break_around_conditional_block.rb b/rubocop/cop/line_break_around_conditional_block.rb new file mode 100644 index 00000000000..3e7021e724e --- /dev/null +++ b/rubocop/cop/line_break_around_conditional_block.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Ensures a line break around conditional blocks. + # + # @example + # # bad + # do_something + # if condition + # do_extra_stuff + # end + # do_something_more + # + # # good + # do_something + # + # if condition + # do_extra_stuff + # end + # + # do_something_more + # + # # bad + # do_something + # unless condition + # do_extra_stuff + # end + # + # do_something_more + # + # # good + # def a_method + # if condition + # do_something + # end + # end + # + # # good + # on_block do + # if condition + # do_something + # end + # end + class LineBreakAroundConditionalBlock < RuboCop::Cop::Cop + MSG = 'Add a line break around conditional blocks' + + def on_if(node) + return if node.single_line? + return unless node.if? || node.unless? + + add_offense(node, location: :expression, message: MSG) unless previous_line_valid?(node) + add_offense(node, location: :expression, message: MSG) unless last_line_valid?(node) + end + + def autocorrect(node) + lambda do |corrector| + line = range_by_whole_lines(node.source_range) + unless previous_line_valid?(node) + corrector.insert_before(line, "\n") + end + + unless last_line_valid?(node) + corrector.insert_after(line, "\n") + end + end + end + + private + + def previous_line_valid?(node) + previous_line(node).empty? || + start_clause_line?(previous_line(node)) || + block_start?(previous_line(node)) || + begin_line?(previous_line(node)) || + assignment_line?(previous_line(node)) + end + + def last_line_valid?(node) + last_line(node).empty? || + end_line?(last_line(node)) || + end_clause_line?(last_line(node)) + end + + def previous_line(node) + processed_source[node.loc.line - 2] + end + + def last_line(node) + processed_source[node.loc.last_line] + end + + def start_clause_line?(line) + line =~ /^\s*(def|=end|#|module|class|if|unless|else|elsif|ensure|when)/ + end + + def end_clause_line?(line) + line =~ /^\s*(rescue|else|elsif|when)/ + end + + def begin_line?(line) + # an assignment followed by a begin or ust a begin + line =~ /^\s*(@?(\w|\|+|=|\[|\]|\s)+begin|begin)/ + end + + def assignment_line?(line) + line =~ /^\s*.*=/ + end + + def block_start?(line) + line.match(/ (do|{)( \|.*?\|)?\s?$/) + end + + def end_line?(line) + line =~ /^\s*(end|})/ + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 2f63babc425..57af87f7fb9 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -1,5 +1,6 @@ require_relative 'cop/gitlab/module_with_instance_variables' require_relative 'cop/include_sidekiq_worker' +require_relative 'cop/line_break_around_conditional_block' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index be19fa93183..775f9db1c6e 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -137,11 +137,14 @@ describe Projects::Clusters::GcpController do context 'when access token is valid' do before do stub_google_api_validate_token + allow_any_instance_of(described_class).to receive(:authorize_google_project_billing) end context 'when google project billing is enabled' do before do - stub_google_project_billing_status + redis_double = double + allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) + allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') end it 'creates a new cluster' do @@ -158,7 +161,7 @@ describe Projects::Clusters::GcpController do it 'renders the cluster form with an error' do go - expect(response).to set_flash[:error] + expect(response).to set_flash[:alert] expect(response).to render_template('new') end end diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 39460834d06..60956511834 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -53,6 +53,7 @@ FactoryBot.define do if evaluator.default_access_level && evaluator.default_push_level protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER) end + if evaluator.default_access_level && evaluator.default_merge_level protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER) end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 587ece22ec7..cf283119f36 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -377,6 +377,7 @@ feature 'Issues > Labels bulk assignment' do items.map do |item| click_link item end + if unmark items.map do |item| # Make sure we are unmarking the item no matter the state it has currently diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 523cc08496b..8953b30bebf 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -13,6 +13,8 @@ feature 'Gcp Cluster', :js do end context 'when user has signed with Google' do + let(:project_id) { 'test-project-1234' } + before do allow_any_instance_of(Projects::Clusters::GcpController) .to receive(:token_in_session).and_return('token') @@ -23,7 +25,7 @@ feature 'Gcp Cluster', :js do context 'when user has a GCP project with billing enabled' do before do allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - stub_google_project_billing_status + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true') end context 'when user does not have a cluster and visits cluster index page' do @@ -131,15 +133,41 @@ feature 'Gcp Cluster', :js do context 'when user does not have a GCP project with billing enabled' do before do + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('false') + visit project_clusters_path(project) click_link 'Add cluster' click_link 'Create on GKE' + + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create cluster' + end + + it 'user sees form with error' do + expect(page).to have_content('Please enable billing for one of your projects to be able to create a cluster, then try again.') + end + end + + context 'when gcp billing status is not in redis' do + before do + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil) + + visit project_clusters_path(project) + + click_link 'Add cluster' + click_link 'Create on GKE' + + fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123' + fill_in 'cluster_name', with: 'dev-cluster' + click_button 'Create cluster' end - it 'user sees a check page' do - pending 'the frontend still has not been implemented' - expect(page).to have_link('Continue') + it 'user sees form with error' do + expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.') end end end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 3f6d16c8acf..0c67196f53e 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -14,7 +14,7 @@ feature 'Multi-file editor new directory', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index ba71eef07f4..85f7318c05d 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -14,7 +14,7 @@ feature 'Multi-file editor new file', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb index 9fbb1dbd0e8..f81e8677e92 100644 --- a/spec/features/projects/tree/upload_file_spec.rb +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -16,7 +16,7 @@ feature 'Multi-file editor upload file', :js do wait_for_requests - click_link('Multi Edit') + click_link('Web IDE') wait_for_requests end diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json index 3d3329a3406..38467b4ca20 100644 --- a/spec/fixtures/api/schemas/entities/issue.json +++ b/spec/fixtures/api/schemas/entities/issue.json @@ -28,7 +28,6 @@ "confidential": { "type": "boolean" }, "discussion_locked": { "type": ["boolean", "null"] }, "updated_by_id": { "type": ["string", "null"] }, - "deleted_at": { "type": ["string", "null"] }, "time_estimate": { "type": "integer" }, "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 7f662098216..05461787f06 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -13,7 +13,6 @@ "updated_by_id": { "type": ["string", "null"] }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, - "deleted_at": { "type": ["string", "null"] }, "time_estimate": { "type": "integer" }, "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["integer", "null"] }, diff --git a/spec/fixtures/api/schemas/public_api/v4/pipelines.json b/spec/fixtures/api/schemas/public_api/v4/pipelines.json new file mode 100644 index 00000000000..8b08a00f708 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pipelines.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "pipeline/basic.json" } +} diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 97e39f6411b..8338efe915b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -256,6 +256,36 @@ describe('AppComponent', () => { }); }); + describe('showLeaveGroupModal', () => { + it('caches candidate group (as props) which is to be left', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.targetGroup).toBe(null); + expect(vm.targetParentGroup).toBe(null); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.targetGroup).not.toBe(null); + expect(vm.targetParentGroup).not.toBe(null); + }); + + it('updates props which show modal confirmation dialog', () => { + const group = Object.assign({}, mockParentGroupItem); + expect(vm.showModal).toBeFalsy(); + expect(vm.groupLeaveConfirmationMessage).toBe(''); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); + }); + }); + + describe('hideLeaveGroupModal', () => { + it('hides modal confirmation which is shown before leaving the group', () => { + const group = Object.assign({}, mockParentGroupItem); + vm.showLeaveGroupModal(group, mockParentGroupItem); + expect(vm.showModal).toBeTruthy(); + vm.hideLeaveGroupModal(); + expect(vm.showModal).toBeFalsy(); + }); + }); + describe('leaveGroup', () => { let groupItem; let childGroupItem; @@ -265,21 +295,24 @@ describe('AppComponent', () => { groupItem.children = mockChildren; childGroupItem = groupItem.children[0]; groupItem.isChildrenLoading = false; + vm.targetGroup = childGroupItem; + vm.targetParentGroup = groupItem; }); - it('should leave group and remove group item from tree', (done) => { + it('hides modal confirmation leave group and remove group item from tree', (done) => { const notice = `You left the "${childGroupItem.fullName}" group.`; spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); spyOn($, 'scrollTo'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); - expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); + vm.leaveGroup(); + expect(vm.showModal).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); - expect(vm.store.removeGroup).toHaveBeenCalledWith(childGroupItem, groupItem); + expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); done(); }, 0); @@ -291,13 +324,13 @@ describe('AppComponent', () => { spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); - vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + vm.leaveGroup(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -309,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(childGroupItem.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(childGroupItem.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); done(); }, 0); }); @@ -364,7 +397,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$on).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); newVm.$destroy(); @@ -404,7 +437,7 @@ describe('AppComponent', () => { Vue.nextTick(() => { expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function)); - expect(eventHub.$off).toHaveBeenCalledWith('leaveGroup', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function)); expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function)); done(); @@ -439,5 +472,14 @@ describe('AppComponent', () => { done(); }); }); + + it('renders modal confirmation dialog', () => { + vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; + vm.showModal = true; + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + }); }); }); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 6d6fb410859..acccbe639c4 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -26,32 +26,12 @@ describe('ItemActionsComponent', () => { vm.$destroy(); }); - describe('computed', () => { - describe('leaveConfirmationMessage', () => { - it('should return appropriate string for leave group confirmation', () => { - expect(vm.leaveConfirmationMessage).toBe('Are you sure you want to leave the "platform / hardware" group?'); - }); - }); - }); - describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.modalStatus).toBeFalsy(); - vm.onLeaveGroup(); - expect(vm.modalStatus).toBeTruthy(); - }); - }); - - describe('leaveGroup', () => { - it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => { spyOn(eventHub, '$emit'); - vm.modalStatus = true; - - vm.leaveGroup(); - - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); + vm.onLeaveGroup(); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', vm.group, vm.parentGroup); }); }); }); @@ -72,7 +52,8 @@ describe('ItemActionsComponent', () => { expect(editBtn.getAttribute('href')).toBe(group.editPath); expect(editBtn.getAttribute('aria-label')).toBe('Edit group'); expect(editBtn.dataset.originalTitle).toBe('Edit group'); - expect(editBtn.querySelector('i.fa.fa-cogs')).toBeDefined(); + expect(editBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings'); newVm.$destroy(); }); @@ -88,17 +69,10 @@ describe('ItemActionsComponent', () => { expect(leaveBtn.getAttribute('href')).toBe(group.leavePath); expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group'); expect(leaveBtn.dataset.originalTitle).toBe('Leave this group'); - expect(leaveBtn.querySelector('i.fa.fa-sign-out')).toBeDefined(); + expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave'); newVm.$destroy(); }); - - it('should show modal dialog when `modalStatus` is set to `true`', () => { - vm.modalStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).toBeDefined(); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); - }); }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 6b608adff15..b020a1020df 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -29,7 +29,6 @@ export const noteableDataMock = { can_create_note: true, can_update: true, }, - deleted_at: null, description: '', due_date: null, human_time_estimate: null, @@ -283,7 +282,6 @@ export const loggedOutnoteableData = { "updated_by_id": 1, "created_at": "2017-02-07T10:11:18.395Z", "updated_at": "2017-08-08T10:22:51.564Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index f90e0093d25..b24563f738b 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -1,4 +1,4 @@ -import OAuthRememberMe from '~/oauth_remember_me'; +import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { preloadFixtures('static/oauth_remember_me.html.raw'); diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index 7f6b5873011..d2386077aa6 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -1,5 +1,5 @@ import '~/lib/utils/text_utility'; -import AbuseReports from '~/abuse_reports'; +import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; describe('Abuse Reports', () => { const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 3b094d20838..7bc591d2d47 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -15,7 +15,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-02-02T21: 49: 49.664Z', updated_at: '2017-05-03T22: 26: 03.760Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, @@ -153,7 +152,6 @@ const RESPONSE_MAP = { updated_by_id: 1, created_at: '2017-06-27T19:54:42.437Z', updated_at: '2017-08-18T03:39:49.222Z', - deleted_at: null, time_estimate: 0, total_time_spent: 0, human_time_estimate: null, diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index a53e8a94d89..c4f500788b2 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -1,5 +1,5 @@ import AccessorUtilities from '~/lib/utils/accessor'; -import SigninTabsMemoizer from '~/signin_tabs_memoizer'; +import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; (() => { describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index ca29c9fee32..ae494267659 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -14,7 +14,6 @@ export default { "updated_by_id": null, "created_at": "2017-04-07T12:27:26.718Z", "updated_at": "2017-04-07T15:39:25.852Z", - "deleted_at": null, "time_estimate": 0, "total_time_spent": 0, "human_time_estimate": null, diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js new file mode 100644 index 00000000000..08e4e1f8337 --- /dev/null +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('clipboard button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(clipboardButton); + vm = mountComponent(Component, { + text: 'copy me', + title: 'Copy this value into Clipboard!', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a button for clipboard', () => { + expect(vm.$el.tagName).toEqual('BUTTON'); + expect(vm.$el.getAttribute('data-clipboard-text')).toEqual('copy me'); + expect(vm.$el.querySelector('i').className).toEqual('fa fa-clipboard'); + }); + + it('should have a tooltip with default values', () => { + expect(vm.$el.getAttribute('data-original-title')).toEqual('Copy this value into Clipboard!'); + expect(vm.$el.getAttribute('data-placement')).toEqual('top'); + expect(vm.$el.getAttribute('data-container')).toEqual(null); + }); +}); diff --git a/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb b/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb new file mode 100644 index 00000000000..21a791f5695 --- /dev/null +++ b/spec/lib/gitlab/background_migration/add_merge_request_diff_commits_count_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount, :migration, schema: 20180105212544 do + let(:projects_table) { table(:projects) } + let(:merge_requests_table) { table(:merge_requests) } + let(:merge_request_diffs_table) { table(:merge_request_diffs) } + let(:merge_request_diff_commits_table) { table(:merge_request_diff_commits) } + + let(:project) { projects_table.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce') } + let(:merge_request) do + merge_requests_table.create!(target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: 'mr name') + end + + def create_diff!(name, commits: 0) + mr_diff = merge_request_diffs_table.create!( + merge_request_id: merge_request.id) + + commits.times do |i| + merge_request_diff_commits_table.create!( + merge_request_diff_id: mr_diff.id, + relative_order: i, sha: i) + end + + mr_diff + end + + describe '#perform' do + it 'migrates diffs that have no commits' do + diff = create_diff!('with_multiple_commits', commits: 0) + + subject.perform(diff.id, diff.id) + + expect(diff.reload.commits_count).to eq(0) + end + + it 'migrates multiple diffs to the correct values' do + diffs = Array.new(3).map.with_index { |_, i| create_diff!(i, commits: 3) } + + subject.perform(diffs.first.id, diffs.last.id) + + diffs.each do |diff| + expect(diff.reload.commits_count).to eq(3) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 84d9e635810..98730602863 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -10,6 +10,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t let(:merge_request_diff) { MergeRequest.find(merge_request.id).create_merge_request_diff } let(:updated_merge_request_diff) { MergeRequestDiff.find(merge_request_diff.id) } + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + def diffs_to_hashes(diffs) diffs.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS).map(&:with_indifferent_access) end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb index dfe3b31f1c0..e99257e3481 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -1,6 +1,12 @@ require 'rails_helper' describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do + # commits_count attribute is added in a next migration + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + describe '#perform' do let(:mr_with_event) { create(:merge_request) } let!(:merged_event) { create(:event, :merged, target: mr_with_event) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f346a345f00..f4e781c599e 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1283,48 +1283,58 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#merged_branch_names' do - context 'when branch names are passed' do - it 'only returns the names we are asking' do - names = repository.merged_branch_names(%w[merge-test]) + shared_examples 'finding merged branch names' do + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) - expect(names).to contain_exactly('merge-test') - end + expect(names).to contain_exactly('merge-test') + end - it 'does not return unmerged branch names' do - names = repository.merged_branch_names(%w[feature]) + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) - expect(names).to be_empty + expect(names).to be_empty + end end - end - context 'when no root ref is available' do - it 'returns empty list' do - project = create(:project, :empty_repo) + context 'when no root ref is available' do + it 'returns empty list' do + project = create(:project, :empty_repo) - names = project.repository.merged_branch_names(%w[feature]) + names = project.repository.merged_branch_names(%w[feature]) - expect(names).to be_empty + expect(names).to be_empty + end end - end - context 'when no branch names are specified' do - before do - repository.create_branch('identical', 'master') - end + context 'when no branch names are specified' do + before do + repository.create_branch('identical', 'master') + end - after do - ensure_seeds - end + after do + ensure_seeds + end - it 'returns all merged branch names except for identical one' do - names = repository.merged_branch_names + it 'returns all merged branch names except for identical one' do + names = repository.merged_branch_names - expect(names).to include('merge-test') - expect(names).to include('fix-mode') - expect(names).not_to include('feature') - expect(names).not_to include('identical') + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + expect(names).not_to include('identical') + end end end + + context 'when Gitaly merged_branch_names feature is enabled' do + it_behaves_like 'finding merged branch names' + end + + context 'when Gitaly merged_branch_names feature is disabled', :disable_gitaly do + it_behaves_like 'finding merged branch names' + end end describe "#ls_files" do diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index aeacd577d18..506b2c0be20 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -14,7 +14,6 @@ describe Gitlab::HookData::IssueBuilder do closed_at confidential created_at - deleted_at description due_date id diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 78475403f9e..b61614e4790 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -12,7 +12,6 @@ describe Gitlab::HookData::MergeRequestBuilder do assignee_id author_id created_at - deleted_at description head_pipeline_id id diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json index 82a1fbd2fc5..1a561e81e4a 100644 --- a/spec/lib/gitlab/import_export/project.group.json +++ b/spec/lib/gitlab/import_export/project.group.json @@ -54,7 +54,6 @@ "iid": 1, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, @@ -134,7 +133,6 @@ "iid": 2, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 7580b62cfc0..4cf33778d15 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -56,7 +56,6 @@ "iid": 10, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "test_ee_field": "test", @@ -350,7 +349,6 @@ "iid": 9, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "milestone": { @@ -586,7 +584,6 @@ "iid": 8, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "label_links": [ @@ -820,7 +817,6 @@ "iid": 7, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1033,7 +1029,6 @@ "iid": 6, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1246,7 +1241,6 @@ "iid": 5, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1459,7 +1453,6 @@ "iid": 4, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1672,7 +1665,6 @@ "iid": 3, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -1885,7 +1877,6 @@ "iid": 2, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -2098,7 +2089,6 @@ "iid": 1, "updated_by_id": null, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "notes": [ @@ -2504,7 +2494,6 @@ "merge_when_pipeline_succeeds": true, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 671, @@ -2948,7 +2937,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 679, @@ -3228,7 +3216,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 777, @@ -3508,7 +3495,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 785, @@ -4198,7 +4184,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 793, @@ -4734,7 +4719,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 801, @@ -5223,7 +5207,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 809, @@ -5478,7 +5461,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 817, @@ -6168,7 +6150,6 @@ "merge_when_pipeline_succeeds": false, "merge_user_id": null, "merge_commit_sha": null, - "deleted_at": null, "notes": [ { "id": 825, @@ -6968,7 +6949,6 @@ "id": 123, "token": "cdbfasdf44a5958c83654733449e585", "project_id": 5, - "deleted_at": null, "created_at": "2017-01-16T15:25:28.637Z", "updated_at": "2017-01-16T15:25:28.637Z" } diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index 02450478a77..5dbf0ed289b 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -54,7 +54,6 @@ "iid": 20, "updated_by_id": 1, "confidential": false, - "deleted_at": null, "due_date": null, "moved_to_id": null, "lock_version": null, diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ec577903eb5..5a33fa3fd53 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -14,7 +14,6 @@ Issue: - iid - updated_by_id - confidential -- deleted_at - closed_at - due_date - moved_to_id @@ -159,7 +158,6 @@ MergeRequest: - merge_when_pipeline_succeeds - merge_user_id - merge_commit_sha -- deleted_at - in_progress_merge_commit_sha - lock_version - milestone_id @@ -180,6 +178,7 @@ MergeRequestDiff: - real_size - head_commit_sha - start_commit_sha +- commits_count MergeRequestDiffCommit: - merge_request_diff_id - relative_order @@ -293,7 +292,6 @@ Ci::Trigger: - id - token - project_id -- deleted_at - created_at - updated_at - owner_id @@ -309,7 +307,6 @@ Ci::PipelineSchedule: - project_id - owner_id - active -- deleted_at - created_at - updated_at Clusters::Cluster: diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 249c77dc636..2e7a0265a0b 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -26,11 +26,16 @@ describe Gitlab::Workhorse do 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys ) end + let(:cache_disabled) { false } subject do described_class.send_git_archive(repository, ref: ref, format: format) end + before do + allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled) + end + context 'when Gitaly workhorse_archive feature is enabled' do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -39,6 +44,15 @@ describe Gitlab::Workhorse do expect(command).to eq('git-archive') expect(params).to include(gitaly_params) end + + context 'when archive caching is disabled' do + let(:cache_disabled) { true } + + it 'tells workhorse not to use the cache' do + _, _, params = decode_workhorse_header(subject) + expect(params).to include({ 'DisableCache' => true }) + end + end end context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do diff --git a/spec/migrations/remove_soft_removed_objects_spec.rb b/spec/migrations/remove_soft_removed_objects_spec.rb new file mode 100644 index 00000000000..ec089f9106d --- /dev/null +++ b/spec/migrations/remove_soft_removed_objects_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171207150343_remove_soft_removed_objects.rb') + +describe RemoveSoftRemovedObjects, :migration do + describe '#up' do + it 'removes various soft removed objects' do + 5.times do + create_with_deleted_at(:issue) + end + + regular_issue = create(:issue) + + run_migration + + expect(Issue.count).to eq(1) + expect(Issue.first).to eq(regular_issue) + end + + it 'removes the temporary indexes once soft removed data has been removed' do + migration = described_class.new + + run_migration + + disable_migrations_output do + expect(migration.temporary_index_exists?(Issue)).to eq(false) + end + end + + it 'removes routes of soft removed personal namespaces' do + namespace = create_with_deleted_at(:namespace) + group = create(:group) + + expect(Route.where(source: namespace).exists?).to eq(true) + expect(Route.where(source: group).exists?).to eq(true) + + run_migration + + expect(Route.where(source: namespace).exists?).to eq(false) + expect(Route.where(source: group).exists?).to eq(true) + end + + it 'schedules the removal of soft removed groups' do + group = create_with_deleted_at(:group) + admin = create(:user, admin: true) + + expect_any_instance_of(GroupDestroyWorker) + .to receive(:perform) + .with(group.id, admin.id) + + run_migration + end + + it 'does not remove soft removed groups when no admin user could be found' do + create_with_deleted_at(:group) + + expect_any_instance_of(GroupDestroyWorker) + .not_to receive(:perform) + + run_migration + end + end + + def run_migration + disable_migrations_output do + migrate! + end + end + + def create_with_deleted_at(*args) + row = create(*args) + + # We set "deleted_at" this way so we don't run into any column cache issues. + row.class.where(id: row.id).update_all(deleted_at: 1.year.ago) + + row + end +end diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb index 2e6b2cff0ab..7494624066a 100644 --- a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb @@ -2,6 +2,12 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb') describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq do + # commits_count attribute is added in a next migration + before do + allow_any_instance_of(MergeRequestDiff) + .to receive(:commits_count=).and_return(nil) + end + let!(:mrs) { create_list(:merge_request, 3) } it 'correctly schedules background migrations' do diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 9a278212efc..8ee15f0e734 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -12,7 +12,6 @@ describe Ci::PipelineSchedule do it { is_expected.to respond_to(:cron_timezone) } it { is_expected.to respond_to(:description) } it { is_expected.to respond_to(:next_run_at) } - it { is_expected.to respond_to(:deleted_at) } describe 'validations' do it 'does not allow invalid cron patters' do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 5ced000cdb6..f5c9f551e65 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -18,11 +18,6 @@ describe Issue do subject { create(:issue) } - describe "act_as_paranoid" do - it { is_expected.to have_db_column(:deleted_at) } - it { is_expected.to have_db_index(:deleted_at) } - end - describe 'callbacks' do describe '#ensure_metrics' do it 'creates metrics after saving' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 07b3e1c1758..27e9c477d61 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -24,11 +24,6 @@ describe MergeRequest do it { is_expected.to include_module(Taskable) } end - describe "act_as_paranoid" do - it { is_expected.to have_db_column(:deleted_at) } - it { is_expected.to have_db_index(:deleted_at) } - end - describe 'validation' do it { is_expected.to validate_presence_of(:target_branch) } it { is_expected.to validate_presence_of(:source_branch) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index b3f160f3119..c3673a0e2a3 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -410,17 +410,6 @@ describe Namespace do end end - describe '#soft_delete_without_removing_associations' do - let(:project1) { create(:project_empty_repo, namespace: namespace) } - - it 'updates the deleted_at timestamp but preserves projects' do - namespace.soft_delete_without_removing_associations - - expect(Project.all).to include(project1) - expect(namespace.deleted_at).not_to be_nil - end - end - describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do expect(namespace.user_ids_for_project_authorizations) diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index ffa17d296e8..f246bb79ab7 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -142,6 +142,7 @@ describe API::CommitStatuses do expect(json_response['ref']).not_to be_empty expect(json_response['target_url']).to be_nil expect(json_response['description']).to be_nil + if status == 'failed' expect(CommitStatus.find(json_response['id'])).to be_api_failure end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0c9fbb1f187..4eae3e50602 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -551,6 +551,49 @@ describe API::MergeRequests do end end + describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do + context 'when authorized' do + let!(:pipeline) { create(:ci_empty_pipeline, project: project, user: user, ref: merge_request.source_branch, sha: merge_request.diff_head_sha) } + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + + it 'returns a paginated array of corresponding pipelines' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines") + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(pipeline.id) + end + + it 'exposes basic attributes' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines") + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pipelines') + end + + it 'returns 404 if MR does not exist' do + get api("/projects/#{project.id}/merge_requests/777/pipelines") + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when unauthorized' do + it 'returns 403' do + project = create(:project, public_builds: false) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.add_guest(guest) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines", guest) + + expect(response).to have_gitlab_http_status(403) + end + end + end + describe "POST /projects/:id/merge_requests" do context 'between branches projects' do it "returns merge_request" do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 679d391caa5..cb66d23b77c 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1142,6 +1142,7 @@ describe API::Runner do else { 'file' => file } end + post api("/jobs/#{job.id}/artifacts"), params, headers end end diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb new file mode 100644 index 00000000000..7ddf9141fcd --- /dev/null +++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb @@ -0,0 +1,411 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/line_break_around_conditional_block' + +describe RuboCop::Cop::LineBreakAroundConditionalBlock do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples 'examples with conditional' do |conditional| + it "flags violation for #{conditional} without line break before" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(2) + expect(cop.highlights).to eq(["#{conditional} condition\n do_something_more\nend"]) + expect(offense.message).to eq('Add a line break around conditional blocks') + end + + it "flags violation for #{conditional} without line break after" do + source = <<~RUBY + #{conditional} condition + do_something + end + do_something_more + RUBY + inspect_source(source) + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(1) + expect(cop.highlights).to eq(["#{conditional} condition\n do_something\nend"]) + expect(offense.message).to eq('Add a line break around conditional blocks') + end + + it "doesn't flag violation for #{conditional} with line break before and after" do + source = <<~RUBY + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a method definition" do + source = <<~RUBY + def a_method + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a class definition" do + source = <<~RUBY + class Foo + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a module definition" do + source = <<~RUBY + module Foo + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a begin definition" do + source = <<~RUBY + begin + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an assign/begin definition" do + source = <<~RUBY + @project ||= begin + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a block definition" do + source = <<~RUBY + on_block(param_a) do |item| + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a block definition using brackets" do + source = <<~RUBY + on_block(param_a) { |item| + #{conditional} condition + do_something + end + } + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a comment" do + source = <<~RUBY + # a short comment + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an assignment" do + source = <<~RUBY + foo = + #{conditional} condition + do_something + else + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a multiline comment" do + source = <<~RUBY + =begin + a multiline comment + =end + #{conditional} condition + do_something + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by another conditional" do + source = <<~RUBY + #{conditional} condition_a + #{conditional} condition_b + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an else" do + source = <<~RUBY + if condition_a + do_something + else + #{conditional} condition_b + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an elsif" do + source = <<~RUBY + if condition_a + do_something + elsif condition_b + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by an ensure" do + source = <<~RUBY + def a_method + ensure + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} preceded by a when" do + source = <<~RUBY + case field + when value + #{conditional} condition_c + do_something_extra + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an end" do + source = <<~RUBY + class Foo + + #{conditional} condition + do_something + end + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an else" do + source = <<~RUBY + #{conditional} condition_a + #{conditional} condition_b + do_something + end + else + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by a when" do + source = <<~RUBY + case + when condition_a + #{conditional} condition_b + do_something + end + when condition_c + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by an elsif" do + source = <<~RUBY + if condition_a + #{conditional} condition_b + do_something + end + elsif condition_c + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{conditional} followed by a rescue" do + source = <<~RUBY + def a_method + #{conditional} condition + do_something + end + rescue + do_something_extra + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end + + it "autocorrects #{conditional} without line break before" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + do_something + + #{conditional} condition + do_something_more + end + RUBY + expect(autocorrected).to eql(expected_source) + end + + it "autocorrects #{conditional} without line break after" do + source = <<~RUBY + #{conditional} condition + do_something + end + do_something_more + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + #{conditional} condition + do_something + end + + do_something_more + RUBY + expect(autocorrected).to eql(expected_source) + end + + it "autocorrects #{conditional} without line break before and after" do + source = <<~RUBY + do_something + #{conditional} condition + do_something_more + end + do_something_extra + RUBY + autocorrected = autocorrect_source(source) + + expected_source = <<~RUBY + do_something + + #{conditional} condition + do_something_more + end + + do_something_extra + RUBY + expect(autocorrected).to eql(expected_source) + end + end + + %w[if unless].each do |example| + it_behaves_like 'examples with conditional', example + end + + it "doesn't flag violation for if with elsif" do + source = <<~RUBY + if condition + do_something + elsif another_condition + do_something_more + end + RUBY + inspect_source(source) + + expect(cop.offenses).to be_empty + end +end diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb index f0e39ba6f49..3e68d906e71 100644 --- a/spec/services/check_gcp_project_billing_service_spec.rb +++ b/spec/services/check_gcp_project_billing_service_spec.rb @@ -1,29 +1,30 @@ require 'spec_helper' describe CheckGcpProjectBillingService do + include GoogleApi::CloudPlatformHelpers + let(:service) { described_class.new } - let(:projects) { [double(name: 'first_project'), double(name: 'second_project')] } + let(:project_id) { 'test-project-1234' } describe '#execute' do before do - expect_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive(:projects_list).and_return(projects) - - allow_any_instance_of(GoogleApi::CloudPlatform::Client) - .to receive_message_chain(:projects_get_billing_info, :billingEnabled) - .and_return(project_billing_enabled) + stub_cloud_platform_projects_list(project_id: project_id) end subject { service.execute('bogustoken') } context 'google account has a billing enabled gcp project' do - let(:project_billing_enabled) { true } + before do + stub_cloud_platform_projects_get_billing_info(project_id, true) + end - it { is_expected.to eq(projects) } + it { is_expected.to all(satisfy { |project| project.project_id == project_id }) } end context 'google account does not have a billing enabled gcp project' do - let(:project_billing_enabled) { false } + before do + stub_cloud_platform_projects_get_billing_info(project_id, false) + end it { is_expected.to eq([]) } end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 53ea88332fb..f3c98fa5416 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -179,13 +179,15 @@ describe Issues::MoveService do { system: true, note: 'Some system note' }, { system: false, note: 'Some comment 2' }] end - + let(:award_names) { %w(thumbsup thumbsdown facepalm) } let(:notes_contents) { notes_params.map { |n| n[:note] } } before do note_params = { noteable: old_issue, project: old_project, author: author } - notes_params.each do |note| - create(:note, note_params.merge(note)) + notes_params.each_with_index do |note, index| + new_note = create(:note, note_params.merge(note)) + award_emoji_params = { awardable: new_note, name: award_names[index] } + create(:award_emoji, award_emoji_params) end end @@ -199,6 +201,10 @@ describe Issues::MoveService do expect(all_notes.pluck(:note).first(3)).to eq notes_contents end + it 'creates new emojis for the new notes' do + expect(all_notes.map(&:award_emoji).to_a.flatten.map(&:name)).to eq award_names + end + it 'adds a system note about move after rewritten notes' do expect(system_notes.last.note).to match /^moved from/ end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index a9605c6e4c6..cb4c3e72aa0 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -171,6 +171,24 @@ describe MergeRequests::BuildService do end end end + + context 'branch starts with external issue IID followed by a hyphen' do + let(:source_branch) { '12345-fix-issue' } + + before do + allow(project).to receive(:external_issue_tracker).and_return(true) + end + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + end + + it 'uses the description of the commit as the description of the merge request and appends the closes text' do + commit_description = commit_1.safe_message.split(/\n+/, 2).last + + expect(merge_request.description).to eq("#{commit_description}\n\nCloses #12345") + end + end end context 'more than one commit in the diff' do @@ -241,8 +259,12 @@ describe MergeRequests::BuildService do allow(project).to receive(:external_issue_tracker).and_return(true) end - it 'sets the title to: Resolves External Issue $issue-iid' do - expect(merge_request.title).to eq('Resolve External Issue 12345') + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('12345 fix issue') + end + + it 'appends the closes text' do + expect(merge_request.description).to eq('Closes #12345') end end end diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index d1b37cdd073..5f047e61c31 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -32,7 +32,7 @@ describe MergeRequests::RebaseService do it 'returns an error' do expect(service.execute(merge_request)).to match(status: :error, - message: 'Failed to rebase. Should be done manually') + message: described_class::REBASE_ERROR) end end @@ -41,15 +41,15 @@ describe MergeRequests::RebaseService do allow(repository).to receive(:run_git!).and_raise('Something went wrong') end - it 'saves the error message' do + it 'saves a generic error message' do subject.execute(merge_request) - expect(merge_request.reload.merge_error).to eq 'Something went wrong' + expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR end it 'returns an error' do expect(service.execute(merge_request)).to match(status: :error, - message: 'Failed to rebase. Should be done manually') + message: described_class::REBASE_ERROR) end end @@ -58,15 +58,15 @@ describe MergeRequests::RebaseService do allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong') end - it 'saves the error message' do + it 'saves a generic error message' do subject.execute(merge_request) - expect(merge_request.reload.merge_error).to eq 'Something went wrong' + expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR end it 'returns an error' do expect(service.execute(merge_request)).to match(status: :error, - message: 'Failed to rebase. Should be done manually') + message: described_class::REBASE_ERROR) end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 39f6388c25e..ef68742a463 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -150,6 +150,7 @@ describe Projects::TransferService do before do group.add_owner(user) + unless gitlab_shell.add_repository(repository_storage, "#{group.full_path}/#{project.path}") raise 'failed to add repository' end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 4e640a82dfc..965fd39c967 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -727,6 +727,7 @@ describe SystemNoteService do else "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/merge_requests/#{merge_request.iid}" end + link = double(object: { 'url' => url }) links << link expect(link).to receive(:save!) diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index aeba9cd60bc..bb3d73edf8e 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -15,7 +15,7 @@ describe Users::DestroyService do expect { user_data['email'].to eq(user.email) } expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) - expect { Namespace.with_deleted.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end it 'will delete the project' do diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index c24940393f9..fa94aa2ae3d 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -113,6 +113,7 @@ shared_examples 'discussion comments' do |resource_name| else expect(find(submit_selector).value).to eq 'Start discussion' end + expect(page).not_to have_selector menu_selector end @@ -200,6 +201,7 @@ shared_examples 'discussion comments' do |resource_name| else expect(find(submit_selector).value).to eq 'Comment' end + expect(page).not_to have_selector menu_selector end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index 05021ea9054..f3f96bd1f0a 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -61,9 +61,11 @@ module FilteredSearchHelpers token_emoji = tokens[index][:emoji_name] expect(el.find('.name')).to have_content(token_name) + if token_value expect(el.find('.value')).to have_content(token_value) end + # gl-emoji content is blank when the emoji unicode is not supported if token_emoji selector = %(gl-emoji[data-name="#{token_emoji}"]) diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb index 4ee33f9725b..876b3b8242d 100755 --- a/spec/support/generate-seed-repo-rb +++ b/spec/support/generate-seed-repo-rb @@ -24,6 +24,7 @@ def main unless system(*%W[git clone --bare #{SOURCE} #{REPO_NAME}], chdir: dir) abort "git clone failed" end + repo = File.join(dir, REPO_NAME) erb = ERB.new(DATA.read) erb.run(binding) diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index 99752ed396e..2fdbddd40c2 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -10,10 +10,14 @@ module GoogleApi request.session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = 1.hour.ago.to_i.to_s end - def stub_google_project_billing_status - redis_double = double - allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) - allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') + def stub_cloud_platform_projects_list(options) + WebMock.stub_request(:get, cloud_platform_projects_list_url) + .to_return(cloud_platform_response(cloud_platform_projects_body(options))) + end + + def stub_cloud_platform_projects_get_billing_info(project_id, billing_enabled) + WebMock.stub_request(:get, cloud_platform_projects_get_billing_info_url(project_id)) + .to_return(cloud_platform_response(cloud_platform_projects_billing_info_body(project_id, billing_enabled))) end def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options) @@ -46,6 +50,14 @@ module GoogleApi .to_return(status: [500, "Internal Server Error"]) end + def cloud_platform_projects_list_url + "https://cloudresourcemanager.googleapis.com/v1/projects" + end + + def cloud_platform_projects_get_billing_info_url(project_id) + "https://cloudbilling.googleapis.com/v1/projects/#{project_id}/billingInfo" + end + def cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id) "https://container.googleapis.com/v1/projects/#{project_id}/zones/#{zone}/clusters/#{cluster_id}" end @@ -121,5 +133,32 @@ module GoogleApi "endTime": options[:endTime] || '' } end + + def cloud_platform_projects_body(**options) + { + "projects": [ + { + "projectNumber": options[:project_number] || "1234", + "projectId": options[:project_id] || "test-project-1234", + "lifecycleState": "ACTIVE", + "name": options[:name] || "test-project", + "createTime": "2017-12-16T01:48:29.129Z", + "parent": { + "type": "organization", + "id": "12345" + } + } + ] + } + end + + def cloud_platform_projects_billing_info_body(project_id, billing_enabled) + { + "name": "projects/#{project_id}/billingInfo", + "projectId": "#{project_id}", + "billingAccountName": "account-name", + "billingEnabled": billing_enabled + } + end end end diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb index cdb62a5deee..42a9ed9ff34 100644 --- a/spec/support/matchers/access_matchers_for_controller.rb +++ b/spec/support/matchers/access_matchers_for_controller.rb @@ -43,6 +43,7 @@ module AccessMatchersForController user = create(:user) membership.public_send(:"add_#{role}", user) end + user end diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index 55da961e173..90618ba5b19 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,6 +17,7 @@ module Select2Helper selector = options.fetch(:from) first(selector, visible: false) + if options[:multiple] execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');") else diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index f621463e621..695152e2d4e 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -4,6 +4,7 @@ module StubENV def stub_env(key_or_hash, value = nil) init_stub unless env_stubbed? + if key_or_hash.is_a? Hash key_or_hash.each { |k, v| add_stubbed_value(k, v) } else diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 664698fcbaf..25ff6094408 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -325,6 +325,7 @@ module TestEnv if component_needs_update?(install_dir, version) # Cleanup the component entirely to ensure we start fresh FileUtils.rm_rf(install_dir) + unless system('rake', task) raise ComponentFailedToInstallError end diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index f4130d68271..fda0e29f983 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -53,6 +53,7 @@ module WaitForRequests wait_until = Time.now + max_wait_time.seconds loop do break if yield + if Time.now > wait_until raise "Condition not met: #{condition_name}" else diff --git a/spec/views/projects/buttons/_dropdown.html.haml_spec.rb b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb new file mode 100644 index 00000000000..d0e692635b9 --- /dev/null +++ b/spec/views/projects/buttons/_dropdown.html.haml_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'projects/buttons/_dropdown' do + let(:user) { create(:user) } + + context 'user with all abilities' do + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:can?).and_return(true) + end + + context 'empty repository' do + let(:project) { create(:project, :empty_repo) } + + it 'has a link to create a new file' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).to have_link('New file') + end + + it 'does not have a link to create a new branch' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).not_to have_link('New branch') + end + + it 'does not have a link to create a new tag' do + render + + expect(view).to render_template('projects/buttons/_dropdown') + expect(rendered).not_to have_link('New tag') + end + end + end +end diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb index f52a903327c..7b7a7c1bc44 100644 --- a/spec/workers/check_gcp_project_billing_worker_spec.rb +++ b/spec/workers/check_gcp_project_billing_worker_spec.rb @@ -8,7 +8,7 @@ describe CheckGcpProjectBillingWorker do context 'when there is a token in redis' do before do - allow_any_instance_of(described_class).to receive(:get_session_token).and_return(token) + allow(described_class).to receive(:get_session_token).and_return(token) end context 'when there is no lease' do |