diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2018-01-11 15:44:35 -0600 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2018-01-11 15:44:35 -0600 |
commit | 15f10ec00a89767abd7e547c96915b526d20e8ee (patch) | |
tree | 73e75e93dd64139a4f7f3a4598e7e011191aaea5 | |
parent | acefbad292e741cb2a60bd3eab81d9b05fd27961 (diff) | |
parent | b5cde6b62efaa97545ebbd334ab96f616a97d326 (diff) | |
download | gitlab-ce-15f10ec00a89767abd7e547c96915b526d20e8ee.tar.gz |
Merge branch 'master' into dispatcher-branches-create
403 files changed, 4627 insertions, 2024 deletions
@@ -8,7 +8,8 @@ "plugins": [ ["istanbul", { "exclude": [ - "spec/javascripts/**/*" + "spec/javascripts/**/*", + "app/assets/javascripts/locale/**/app.js" ] }], ["transform-define", { diff --git a/.eslintrc b/.eslintrc index 6dbe269e594..ad5eaebccae 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,6 @@ ], "globals": { "__webpack_public_path__": true, - "_": false, "gl": false, "gon": false, "localStorage": false 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/CHANGELOG.md b/CHANGELOG.md index 26580e7183f..c29c289310d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.3.4 (2018-01-10) + +### Security (7 changes, 1 of them is from the community) + +- Prevent a SQL injection in the MilestonesFinder. +- Fix RCE via project import mechanism. +- Prevent OAuth login POST requests when a provider has been disabled. +- Filter out sensitive fields from the project services API. (Robert Schilling) +- Check user authorization for source and target projects when creating a merge request. +- Fix path traversal in gitlab-ci.yml cache:key. +- Fix writable shared deploy keys. + + ## 10.3.3 (2018-01-02) ### Fixed (3 changes) @@ -180,6 +193,21 @@ entry. - Clean up schema of the "merge_requests" table. +## 10.2.6 (2018-01-11) + +### Security (9 changes, 1 of them is from the community) + +- Fix writable shared deploy keys. +- Filter out sensitive fields from the project services API. (Robert Schilling) +- Fix RCE via project import mechanism. +- Fixed IPython notebook output not being sanitized. +- Prevent OAuth login POST requests when a provider has been disabled. +- Prevent a SQL injection in the MilestonesFinder. +- Check user authorization for source and target projects when creating a merge request. +- Fix path traversal in gitlab-ci.yml cache:key. +- Fix XSS vulnerability in pipeline job trace. + + ## 10.2.5 (2017-12-15) ### Fixed (8 changes) @@ -446,6 +474,20 @@ entry. - Add Gitaly metrics to the performance bar. +## 10.1.6 (2018-01-11) + +### Security (8 changes, 1 of them is from the community) + +- Fix writable shared deploy keys. +- Filter out sensitive fields from the project services API. (Robert Schilling) +- Fix RCE via project import mechanism. +- Prevent OAuth login POST requests when a provider has been disabled. +- Prevent a SQL injection in the MilestonesFinder. +- Check user authorization for source and target projects when creating a merge request. +- Fix path traversal in gitlab-ci.yml cache:key. +- Fix XSS vulnerability in pipeline job trace. + + ## 10.1.5 (2017-12-07) ### Security (5 changes) 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/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index f7ae6f1cd12..83cac896f86 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -4,6 +4,8 @@ import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; +Dropzone.autoDiscover = false; + function toggleLoading($el, $icon, loading) { if (loading) { $el.disable(); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index adb7360327c..a8dafd31f12 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list'; diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index d8cf532fe78..591f1dc8313 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,4 +1,4 @@ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 25cef44c1b8..57457ebd0a3 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -46,14 +46,15 @@ )); const extraCostParagraph = sprintf( - _.escape(s__(`ClusterIntegration|%{boldNotice} This will add some -extra resources like a load balancer, -which incur additional costs. See %{pricingLink}`)), - { + _.escape(s__( + `ClusterIntegration|%{boldNotice} This will add some extra resources + like a load balancer, which may incur additional costs depending on + the hosting provider Kubernetes is installed on. If you are using GKE, + you can %{pricingLink}.`, + )), { boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|GKE pricing'))} - </a>`, + ${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`, }, false, ); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index eedbd3feeb5..bc23a72762f 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,4 +1,5 @@ /* eslint-disable no-new */ +import _ from 'underscore'; import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 10c4931b7d6..a1e0c42e711 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,43 +32,31 @@ 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'; 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,18 +140,17 @@ 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': - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + import('./pages/projects/boards/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:merge_requests:index': case 'projects:issues:index': @@ -209,8 +191,9 @@ import Activities from './activities'; .catch(fail); break; case 'dashboard:merge_requests': - projectSelect(); - initLegacyFilters(); + import('./pages/dashboard/merge_requests') + .then(callDefault) + .catch(fail); break; case 'groups:issues': case 'groups:merge_requests': @@ -232,19 +215,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': @@ -355,7 +333,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(); @@ -376,8 +356,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); @@ -409,23 +391,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(); @@ -447,22 +422,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': @@ -519,22 +500,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 @@ -573,8 +562,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') @@ -608,29 +603,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; @@ -639,8 +656,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(); @@ -653,8 +671,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/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index c84be42649a..550dbdda922 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -3,6 +3,8 @@ import _ from 'underscore'; import './preview_markdown'; import csrf from './lib/utils/csrf'; +Dropzone.autoDiscover = false; + export default function dropzoneInput(form) { const divHover = '<div class="div-dropzone-hover"></div>'; const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 7219b076721..34d18d55120 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -1,7 +1,7 @@ /** * Common code between environmets app and folder view */ - +import _ from 'underscore'; import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 46c80dfd45e..ff046aa286a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c05a83176f2..58ed0012f01 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 6139e81fe6d..2e859d2de3a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '../lib/utils/ajax_cache'; import Flash from '../flash'; import FilteredSearchContainer from './container'; 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/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 51e202b9348..668221c0296 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 29e3ab5d040..d556404faa5 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export const dataStructure = () => ({ id: '', key: '', diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index c929dc98c10..ac2f636df0f 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ -/* global Sortable */ +import Sortable from 'vendor/Sortable'; import Flash from './flash'; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index ce6f91439b4..d8b881a8fac 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,33 +1,17 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ +/* eslint-disable import/first */ /* global ConfirmDangerModal */ import jQuery from 'jquery'; -import _ from 'underscore'; import Cookies from 'js-cookie'; -import Dropzone from 'dropzone'; -import Sortable from 'vendor/Sortable'; import svg4everybody from 'svg4everybody'; -// libraries with import side-effects -import 'mousetrap'; -import 'mousetrap/plugins/pause/mousetrap-pause'; - // expose common libraries as globals (TODO: remove these) window.jQuery = jQuery; window.$ = jQuery; -window._ = _; -window.Dropzone = Dropzone; -window.Sortable = Sortable; - -// templates -import './templates/issuable_template_selector'; -import './templates/issuable_template_selectors'; - -import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; -import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility'; +import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors @@ -43,7 +27,6 @@ import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import initLayoutNav from './layout_nav'; import LazyLoader from './lazy_loader'; -import './line_highlighter'; import initLogoAnimation from './logo'; import './milestone_select'; import './projects_dropdown'; @@ -55,11 +38,9 @@ import './dispatcher'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); -Dropzone.autoDiscover = false; - svg4everybody(); -document.addEventListener('beforeunload', function () { +document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips @@ -76,16 +57,15 @@ window.addEventListener('load', function onLoad() { gl.lazyLoader = new LazyLoader({ scrollContainer: window, - observerNode: '#content-body' + observerNode: '#content-body', }); -$(function () { - var $body = $('body'); - var $document = $(document); - var $window = $(window); - var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var bootstrapBreakpoint = bp.getBreakpointSize(); - var fitSidebarForSize; +$(() => { + const $body = $('body'); + const $document = $(document); + const $window = $(window); + const $sidebarGutterToggle = $('.js-sidebar-toggle'); + let bootstrapBreakpoint = bp.getBreakpointSize(); initBreadcrumbs(); initLayoutNav(); @@ -97,8 +77,8 @@ $(function () { Cookies.defaults.path = gon.relative_url_root || '/'; // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function() { - var href = this.getAttribute('href'); + $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() { + const href = this.getAttribute('href'); if (href.substr(1) === getLocationHash()) { setTimeout(handleLocationHash, 1); } @@ -113,155 +93,162 @@ $(function () { } // prevent default action for disabled buttons - $('.btn').click(function(e) { + $('.btn').click(function clickDisabledButtonCallback(e) { if ($(this).hasClass('disabled')) { e.preventDefault(); e.stopImmediatePropagation(); return false; } + + return true; }); - $('.js-select-on-focus').on('focusin', function () { - return $(this).select().one('mouseup', function (e) { - return e.preventDefault(); - }); // Click a .js-select-on-focus field, select the contents // Prevent a mouseup event from deselecting the input + $('.js-select-on-focus').on('focusin', function selectOnFocusCallback() { + $(this).select().one('mouseup', (e) => { + e.preventDefault(); + }); }); - $('.remove-row').bind('ajax:success', function () { + + $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this).tooltip('destroy') .closest('li') .fadeOut(); }); - $('.js-remove-tr').bind('ajax:before', function () { - return $(this).hide(); + + $('.js-remove-tr').on('ajax:before', function removeTRAjaxBeforeCallback() { + $(this).hide(); }); - $('.js-remove-tr').bind('ajax:success', function () { - return $(this).closest('tr').fadeOut(); + + $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { + $(this).closest('tr').fadeOut(); }); + + // Initialize select2 selects $('select.select2').select2({ width: 'resolve', - // Initialize select2 selects - dropdownAutoWidth: true + dropdownAutoWidth: true, }); - $('.js-select2').bind('select2-close', function () { - return setTimeout((function () { - $('.select2-container-active').removeClass('select2-container-active'); - return $(':focus').blur(); - }), 1); + // Close select2 on escape + $('.js-select2').on('select2-close', () => { + setTimeout(() => { + $('.select2-container-active').removeClass('select2-container-active'); + $(':focus').blur(); + }, 1); }); + // Initialize tooltips $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', - placement: function (tip, el) { + placement(tip, el) { return $(el).data('placement') || 'bottom'; - } + }, }); + // Initialize popovers $body.popover({ selector: '[data-toggle="popover"]', trigger: 'focus', // set the viewport to the main content, excluding the navigation bar, so // the navigation can't overlap the popover - viewport: '.layout-page' + viewport: '.layout-page', }); - $('.trigger-submit').on('change', function () { - return $(this).parents('form').submit(); + // Form submitter + $('.trigger-submit').on('change', function triggerSubmitCallback() { + $(this).parents('form').submit(); }); + localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Disable form buttons while a form is submitting - $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { - var buttons; - buttons = $('[type="submit"], .js-disable-on-submit', this); + $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) { + const $buttons = $('[type="submit"], .js-disable-on-submit', this); switch (e.type) { case 'ajax:beforeSend': case 'submit': - return buttons.disable(); + return $buttons.disable(); default: - return buttons.enable(); + return $buttons.enable(); } }); - $(document).ajaxError(function (e, xhrObj) { - var ref = xhrObj.status; - if (xhrObj.status === 401) { - return new Flash('You need to be logged in.', 'alert'); + + $(document).ajaxError((e, xhrObj) => { + const ref = xhrObj.status; + + if (ref === 401) { + Flash('You need to be logged in.'); } else if (ref === 404 || ref === 500) { - return new Flash('Something went wrong on our end.', 'alert'); + Flash('Something went wrong on our end.'); } }); - $('.account-box').hover(function () { - // Show/Hide the profile menu when hovering the account box - return $(this).toggleClass('hover'); - }); - $document.on('click', '.diff-content .js-show-suppressed-diff', function () { - var $container; - $container = $(this).parent(); - $container.next('table').show(); - return $container.remove(); + // Commit show suppressed diff + $document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() { + const $container = $(this).parent(); + $container.next('table').show(); + $container.remove(); }); + $('.navbar-toggle').on('click', () => { $('.header-content').toggleClass('menu-expanded'); gl.lazyLoader.loadCheck(); }); + // Show/hide comments on diff - $body.on('click', '.js-toggle-diff-comments', function (e) { - var $this = $(this); - var notesHolders = $this.closest('.diff-file').find('.notes_holder'); + $body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) { + const $this = $(this); + const notesHolders = $this.closest('.diff-file').find('.notes_holder'); + + e.preventDefault(); + $this.toggleClass('active'); + if ($this.hasClass('active')) { notesHolders.show().find('.hide, .content').show(); } else { notesHolders.hide().find('.content').hide(); } + $(document).trigger('toggle.comments'); - return e.preventDefault(); }); - $document.off('click', '.js-confirm-danger'); - $document.on('click', '.js-confirm-danger', function (e) { - var btn = $(e.target); - var form = btn.closest('form'); - var text = btn.data('confirm-danger-message'); + + $document.on('click', '.js-confirm-danger', (e) => { + const btn = $(e.target); + const form = btn.closest('form'); + const text = btn.data('confirm-danger-message'); e.preventDefault(); - return new ConfirmDangerModal(form, text); - }); - $('input[type="search"]').each(function () { - var $this = $(this); - $this.attr('value', $this.val()); - }); - $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function () { - var $this; - $this = $(this); - return $this.attr('value', $this.val()); + + // eslint-disable-next-line no-new + new ConfirmDangerModal(form, text); }); - $document.off('breakpoint:change').on('breakpoint:change', function (e, breakpoint) { - var $gutterIcon; + + $document.on('breakpoint:change', (e, breakpoint) => { if (breakpoint === 'sm' || breakpoint === 'xs') { - $gutterIcon = $sidebarGutterToggle.find('i'); + const $gutterIcon = $sidebarGutterToggle.find('i'); if ($gutterIcon.hasClass('fa-angle-double-right')) { - return $sidebarGutterToggle.trigger('click'); + $sidebarGutterToggle.trigger('click'); } } }); - fitSidebarForSize = function () { - var oldBootstrapBreakpoint; - oldBootstrapBreakpoint = bootstrapBreakpoint; + + function fitSidebarForSize() { + const oldBootstrapBreakpoint = bootstrapBreakpoint; bootstrapBreakpoint = bp.getBreakpointSize(); + if (bootstrapBreakpoint !== oldBootstrapBreakpoint) { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); + $document.trigger('breakpoint:change', [bootstrapBreakpoint]); } - }; - $window.off('resize.app').on('resize.app', function () { - return fitSidebarForSize(); - }); - loadAwardsHandler(); + } - renderTimeago(); + $window.on('resize.app', fitSidebarForSize); + + loadAwardsHandler(); - $('form.filter-form').on('submit', function (event) { + $('form.filter-form').on('submit', function filterFormSubmitCallback(event) { const link = document.createElement('a'); link.href = this.action; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index f76a998bf8c..dd6c6b854bc 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,5 +1,3 @@ -/* global Sortable */ - import Flash from './flash'; export default class Milestone { 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..857a6793fe3 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export default function initBroadcastMessagesForm() { $('input#broadcast_message_color').on('input', function onMessageColorInput() { const previewColor = $(this).val(); 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/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; 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/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js new file mode 100644 index 00000000000..42c9bb5ec99 --- /dev/null +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -0,0 +1,7 @@ +import UsersSelect from '~/users_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default () => { + new UsersSelect(); // eslint-disable-line no-new + new ShortcutsNavigation(); // 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/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js index 9cbd8f21f2a..7231f520933 100644 --- a/app/assets/javascripts/projects_dropdown/service/projects_service.js +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index cf309be4f6f..908b9cab93d 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 81286c0010c..1e246a56b85 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsFindFile extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 292e3d6a657..6aeae84cdc5 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,7 +1,5 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import _ from 'underscore'; -import 'mousetrap'; import Sidebar from './right_sidebar'; import ShortcutsNavigation from './shortcuts_navigation'; import { CopyAsGFM } from './behaviors/copy_as_gfm'; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index b4562701a3e..a4d10850471 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,5 +1,4 @@ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import findAndFollowLink from './shortcuts_dashboard_navigation'; import Shortcuts from './shortcuts'; diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 21823085ac4..a88c280fa3b 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; export default class ShortcutsNetwork extends ShortcutsNavigation { diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/shortcuts_wiki.js index 59b967dbe09..41865dcf4ba 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/shortcuts_wiki.js @@ -1,16 +1,14 @@ -/* eslint-disable class-methods-use-this */ -/* global Mousetrap */ - +import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation'; export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { super(); - Mousetrap.bind('e', this.editWiki); + Mousetrap.bind('e', ShortcutsWiki.editWiki); } - editWiki() { + static editWiki() { findAndFollowLink('.js-wiki-edit'); } } 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/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index 06a86f3b94a..4592003f57e 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */ -/* global Mousetrap */ // Zen Mode (full screen) textarea // @@ -8,9 +7,11 @@ import 'vendor/jquery.scrollTo'; import Dropzone from 'dropzone'; -import 'mousetrap'; +import Mousetrap from 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; +Dropzone.autoDiscover = false; + // // ### Events // 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/diff.scss b/app/assets/stylesheets/pages/diff.scss index 1d081b58f62..7f037582ca0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -651,12 +651,18 @@ min-width: 0; } - .diff-changed-file-name { + .diff-changed-file-name, + .diff-changed-blank-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .diff-changed-blank-file-name { + color: $gl-text-color-tertiary; + font-style: italic; + } + .diff-changed-file-path { color: $gl-text-color-tertiary; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a35ebd48887..370b07663fd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -798,7 +798,6 @@ button.mini-pipeline-graph-dropdown-toggle { // link to the build .mini-pipeline-graph-dropdown-item { - padding: 3px 7px 4px; align-items: center; clear: both; display: flex; 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 fbe65e700a4..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) @@ -1149,7 +1150,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.write_ref('HEAD', "refs/heads/#{branch}") + repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false) repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch 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 b36e756c07c..d27212b2058 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -260,7 +260,7 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - write_ref(keep_around_ref_name(sha), sha) + raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false) rescue Rugged::ReferenceError => ex Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex @@ -274,10 +274,6 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) - end - def diverging_commit_counts(branch) root_ref_hash = raw_repository.commit(root_ref).id cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -1018,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/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index adc4dcbed33..0b01e38d23d 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -11,7 +11,7 @@ - unless diff_file.submodule? - blob = diff_file.blob .file-actions.hidden-xs - - if blob.readable_text? + - if blob&.readable_text? = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 325159dd9a7..b082ad0ef0e 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -24,7 +24,12 @@ %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8") %span.diff-changed-file-content.append-right-8 - %strong.diff-changed-file-name= diff_file.blob.name + - if diff_file.blob&.name + %strong.diff-changed-file-name + = diff_file.blob.name + - else + %strong.diff-changed-blank-file-name + = s_('Diffs|No file name available') %span.diff-changed-file-path.prepend-top-5= diff_file_path_text(diff_file) %span.diff-changed-stats %span.cgreen< 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/41163-improve-cluster-ingress-extra-cost-language.yml b/changelogs/unreleased/41163-improve-cluster-ingress-extra-cost-language.yml new file mode 100644 index 00000000000..9c48831855c --- /dev/null +++ b/changelogs/unreleased/41163-improve-cluster-ingress-extra-cost-language.yml @@ -0,0 +1,5 @@ +--- +title: Improve wording about additional costs for Ingress on custom clusters +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/41491-fix-nil-blob-name-error.yml b/changelogs/unreleased/41491-fix-nil-blob-name-error.yml new file mode 100644 index 00000000000..cf7e63ea46a --- /dev/null +++ b/changelogs/unreleased/41491-fix-nil-blob-name-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 error when visiting a commit where the blobs do not exist +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/auth/crowd.md b/doc/administration/auth/crowd.md index 2c289c67a6d..6db74958d5a 100644 --- a/doc/administration/auth/crowd.md +++ b/doc/administration/auth/crowd.md @@ -66,3 +66,15 @@ On the sign in page there should now be a Crowd tab in the sign in form. [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../restart_gitlab.md#installations-from-source + +## Troubleshooting + +If you see an error message like the one below when you sign in after Crowd authentication is configured, you may want to consult the Crowd administrator for the Crowd log file to know the exact cause: + +``` +could not authorize you from Crowd because invalid credentials +``` + +Please make sure the Crowd users who need to login to GitLab are authorized to [the application](#configure-a-new-crowd-application) in the step of **Authorisation**. This could be verified by try "Authentication test" for Crowd as of 2.11. + +![Example Crowd application authorisation configuration](img/crowd_application_authorisation.png)
\ No newline at end of file diff --git a/doc/administration/auth/img/crowd_application_authorisation.png b/doc/administration/auth/img/crowd_application_authorisation.png Binary files differnew file mode 100644 index 00000000000..70339891b34 --- /dev/null +++ b/doc/administration/auth/img/crowd_application_authorisation.png 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/articles/how_to_install_git/index.md b/doc/articles/how_to_install_git/index.md index 37b60501ce2..3e6003a33b7 100644 --- a/doc/articles/how_to_install_git/index.md +++ b/doc/articles/how_to_install_git/index.md @@ -1,66 +1 @@ -# Installing Git - -> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** user guide || -> **Level:** beginner || -> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) || -> **Publication date:** 2017-05-15 - -To begin contributing to GitLab projects -you will need to install the Git client on your computer. -This article will show you how to install Git on macOS, Ubuntu Linux and Windows. - -## Install Git on macOS using the Homebrew package manager - -Although it is easy to use the version of Git shipped with macOS -or install the latest version of Git on macOS by downloading it from the project website, -we recommend installing it via Homebrew to get access to -an extensive selection of dependancy managed libraries and applications. - -If you are sure you don't need access to any additional development libraries -or don't have approximately 15gb of available disk space for Xcode and Homebrew -use one of the the aforementioned methods. - -### Installing Xcode - -Xcode is needed by Homebrew to build dependencies. -You can install [XCode](https://developer.apple.com/xcode/) -through the macOS App Store. - -### Installing Homebrew - -Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html) -for the official Homebrew installation instructions. - -### Installing Git via Homebrew - -With Homebrew installed you are now ready to install Git. -Open a Terminal and enter in the following command: - -```bash -brew install git -``` - -Congratulations you should now have Git installed via Homebrew. -Next read our article on [adding an SSH key to GitLab](../../ssh/README.md). - -## Install Git on Ubuntu Linux - -On Ubuntu and other Linux operating systems -it is recommended to use the built in package manager to install Git. - -Open a Terminal and enter in the following commands -to install the latest Git from the official Git maintained package archives: - -```bash -sudo apt-add-repository ppa:git-core/ppa -sudo apt-get update -sudo apt-get install git -``` - -Congratulations you should now have Git installed via the Ubuntu package manager. -Next read our article on [adding an SSH key to GitLab](../../ssh/README.md). - -## Installing Git on Windows from the Git website - -Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows. -Next read our article on [adding an SSH key to GitLab](../../ssh/README.md). +This document was moved to [another location](../../topics/git/how_to_install_git/index.md). diff --git a/doc/articles/index.md b/doc/articles/index.md index 06675e15d76..8385ef936c6 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -17,7 +17,6 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM | Article title | Category | Publishing date | | :------------ | :------: | --------------: | | [Autoscaling GitLab Runners on AWS](runner_autoscale_aws/index.md) | Admin guide | 2017-11-24 | -| [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) | Tutorial | 2017-08-31 | | [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 | | [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017-07-11 | | [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017-07-27 | @@ -30,18 +29,6 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM | [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) | Technical overview | 2017-05-15 | | [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) | Tutorial | 2016-03-10 | -## Git - -Learn how to use [Git with GitLab](../topics/git/index.md): - -| Article title | Category | Publishing date | -| :------------ | :------: | --------------: | -| [Numerous _undo_ possibilities in Git](numerous_undo_possibilities_in_git/index.md) | Tutorial | 2017-08-17 | -| [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/) | Concepts | 2017-05-17 | -| [How to install Git](how_to_install_git/index.md) | Tutorial | 2017-05-15 | -| [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) | Tutorial | 2017-01-30 | -| [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) | Technical overview | 2016-12-08 | - ## GitLab Pages Learn how to deploy a static website with [GitLab Pages](../user/project/pages/index.md#getting-started): @@ -74,7 +61,6 @@ upgrade, integrate, migrate to GitLab: | :------------ | :------: | --------------: | | [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017-01-23 | | [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016-07-13 | -| [Get started with OpenShift Origin 3 and GitLab](openshift_and_gitlab/index.md) | Tutorial | 2016-06-28 | | [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016-04-27 | ## Software development diff --git a/doc/articles/laravel_with_gitlab_and_envoy/index.md b/doc/articles/laravel_with_gitlab_and_envoy/index.md index b20bd8c247a..b092cdb0f7a 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/index.md +++ b/doc/articles/laravel_with_gitlab_and_envoy/index.md @@ -1,680 +1 @@ -# Test and deploy Laravel applications with GitLab CI/CD and Envoy - -> **[Article Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Mehran Rasulian](https://gitlab.com/mehranrasulian) || -> **Publication date:** 2017-08-31 - -## Introduction - -GitLab features our applications with Continuous Integration, and it is possible to easily deploy the new code changes to the production server whenever we want. - -In this tutorial, we'll show you how to initialize a [Laravel](http://laravel.com/) application and setup our [Envoy](https://laravel.com/docs/envoy) tasks, then we'll jump into see how to test and deploy it with [GitLab CI/CD](../../ci/README.md) via [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/). - -We assume you have a basic experience with Laravel, Linux servers, -and you know how to use GitLab. - -Laravel is a high quality web framework written in PHP. -It has a great community with a [fantastic documentation](https://laravel.com/docs). -Aside from the usual routing, controllers, requests, responses, views, and (blade) templates, out of the box Laravel provides plenty of additional services such as cache, events, localization, authentication and many others. - -We will use [Envoy](https://laravel.com/docs/master/envoy) as an SSH task runner based on PHP. -It uses a clean, minimal [Blade syntax](https://laravel.com/docs/blade) to setup tasks that can run on remote servers, such as, cloning your project from the repository, installing the Composer dependencies, and running [Artisan commands](https://laravel.com/docs/artisan). - -## Initialize our Laravel app on GitLab - -We assume [you have installed a new laravel project](https://laravel.com/docs/installation#installation), so let's start with a unit test, and initialize Git for the project. - -### Unit Test - -Every new installation of Laravel (currently 5.4) comes with two type of tests, 'Feature' and 'Unit', placed in the tests directory. -Here's a unit test from `test/Unit/ExampleTest.php`: - -```php -<?php - -namespace Tests\Unit; - -... - -class ExampleTest extends TestCase -{ - public function testBasicTest() - { - $this->assertTrue(true); - } -} -``` - -This test is as simple as asserting that the given value is true. - -Laravel uses `PHPUnit` for tests by default. -If we run `vendor/bin/phpunit` we should see the green output: - -```bash -vendor/bin/phpunit -OK (1 test, 1 assertions) -``` - -This test will be used later for continuously testing our app with GitLab CI/CD. - -### Push to GitLab - -Since we have our app up and running locally, it's time to push the codebase to our remote repository. -Let's create [a new project](../../gitlab-basics/create-project.md) in GitLab named `laravel-sample`. -After that, follow the command line instructions displayed on the project's homepage to initiate the repository on our machine and push the first commit. - - -```bash -cd laravel-sample -git init -git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git -git add . -git commit -m 'Initial Commit' -git push -u origin master -``` - -## Configure the production server - -Before we begin setting up Envoy and GitLab CI/CD, let's quickly make sure the production server is ready for deployment. -We have installed LEMP stack which stands for Linux, Nginx, MySQL and PHP on our Ubuntu 16.04. - -### Create a new user - -Let's now create a new user that will be used to deploy our website and give it -the needed permissions using [Linux ACL](https://serversforhackers.com/video/linux-acls): - -```bash -# Create user deployer -sudo adduser deployer -# Give the read-write-execute permissions to deployer user for directory /var/www -sudo setfacl -R -m u:deployer:rwx /var/www -``` - -If you don't have ACL installed on your Ubuntu server, use this command to install it: - -```bash -sudo apt install acl -``` - -### Add SSH key - -Let's suppose we want to deploy our app to the production server from a private repository on GitLab. First, we need to [generate a new SSH key pair **with no passphrase**](../../ssh/README.md) for the deployer user. - -After that, we need to copy the private key, which will be used to connect to our server as the deployer user with SSH, to be able to automate our deployment process: - -```bash -# As the deployer user on server -# -# Copy the content of public key to authorized_keys -cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys -# Copy the private key text block -cat ~/.ssh/id_rsa -``` - -Now, let's add it to your GitLab project as a [secret variable](../../ci/variables/README.md#secret-variables). -Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. -They can be added per project by navigating to the project's **Settings** > **CI/CD**. - -![secret variables page](img/secret_variables_page.png) - -To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier. -We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password. - -We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../ssh/README.md/#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../gitlab-basics/command-line-commands.md/#start-working-on-your-project). - - -```bash -# As the deployer user on the server -# -# Copy the public key -cat ~/.ssh/id_rsa.pub -``` - -![deploy keys page](img/deploy_keys_page.png) - -To the field **Title**, add any name you want, and paste the public key into the **Key** field. - -Now, let's clone our repository on the server just to make sure the `deployer` user has access to the repository. - -```bash -# As the deployer user on server -# -git clone git@gitlab.example.com:<USERNAME>/laravel-sample.git -``` - ->**Note:** -Answer **yes** if asked `Are you sure you want to continue connecting (yes/no)?`. -It adds GitLab.com to the known hosts. - -### Configuring Nginx - -Now, let's make sure our web server configuration points to the `current/public` rather than `public`. - -Open the default Nginx server block configuration file by typing: - -```bash -sudo nano /etc/nginx/sites-available/default -``` - -The configuration should be like this. - -``` -server { - root /var/www/app/current/public; - server_name example.com; - # Rest of the configuration -} -``` - ->**Note:** -You may replace the app's name in `/var/www/app/current/public` with the folder name of your application. - -## Setting up Envoy - -So we have our Laravel app ready for production. -The next thing is to use Envoy to perform the deploy. - -To use Envoy, we should first install it on our local machine [using the given instructions by Laravel](https://laravel.com/docs/envoy/#introduction). - -### How Envoy works - -The pros of Envoy is that it doesn't require Blade engine, it just uses Blade syntax to define tasks. -To start, we create an `Envoy.blade.php` in the root of our app with a simple task to test Envoy. - - -```php -@servers(['web' => 'remote_username@remote_host']) - -@task('list', [on => 'web']) - ls -l -@endtask -``` - -As you may expect, we have an array within `@servers` directive at the top of the file, which contains a key named `web` with a value of the server's address (e.g. `deployer@192.168.1.1`). -Then within our `@task` directive we define the bash commands that should be run on the server when the task is executed. - -On the local machine use the `run` command to run Envoy tasks. - -```bash -envoy run list -``` - -It should execute the `list` task we defined earlier, which connects to the server and lists directory contents. - -Envoy is not a dependency of Laravel, therefore you can use it for any PHP application. - -### Zero downtime deployment - -Every time we deploy to the production server, Envoy downloads the latest release of our app from GitLab repository and replace it with preview's release. -Envoy does this without any [downtime](https://en.wikipedia.org/wiki/Downtime), -so we don't have to worry during the deployment while someone might be reviewing the site. -Our deployment plan is to clone the latest release from GitLab repository, install the Composer dependencies and finally, activate the new release. - -#### @setup directive - -The first step of our deployment process is to define a set of variables within [@setup](https://laravel.com/docs/envoy/#setup) directive. -You may change the `app` to your application's name: - - -```php -... - -@setup - $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git'; - $releases_dir = '/var/www/app/releases'; - $app_dir = '/var/www/app'; - $release = date('YmdHis'); - $new_release_dir = $releases_dir .'/'. $release; -@endsetup - -... -``` - -- `$repository` is the address of our repository -- `$releases_dir` directory is where we deploy the app -- `$app_dir` is the actual location of the app that is live on the server -- `$release` contains a date, so every time that we deploy a new release of our app, we get a new folder with the current date as name -- `$new_release_dir` is the full path of the new release which is used just to make the tasks cleaner - -#### @story directive - -The [@story](https://laravel.com/docs/envoy/#stories) directive allows us define a list of tasks that can be run as a single task. -Here we have three tasks called `clone_repository`, `run_composer`, `update_symlinks`. These variables are usable to making our task's codes more cleaner: - - -```php -... - -@story('deploy') - clone_repository - run_composer - update_symlinks -@endstory - -... -``` - -Let's create these three tasks one by one. - -#### Clone the repository - -The first task will create the `releases` directory (if it doesn't exist), and then clone the `master` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable. -The `releases` directory will hold all our deployments: - -```php -... - -@task('clone_repository') - echo 'Cloning repository' - [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} - git clone --depth 1 {{ $repository }} {{ $new_release_dir }} -@endtask - -... -``` - -While our project grows, its Git history will be very very long over time. -Since we are creating a directory per release, it might not be necessary to have the history of the project downloaded for each release. -The `--depth 1` option is a great solution which saves systems time and disk space as well. - -#### Installing dependencies with Composer - -As you may know, this task just navigates to the new release directory and runs Composer to install the application dependencies: - -```php -... - -@task('run_composer') - echo "Starting deployment ({{ $release }})" - cd {{ $new_release_dir }} - composer install --prefer-dist --no-scripts -q -o -@endtask - -... -``` - -#### Activate new release - -Next thing to do after preparing the requirements of our new release, is to remove the storage directory from it and to create two symbolic links to point the application's `storage` directory and `.env` file to the new release. -Then, we need to create another symbolic link to the new release with the name of `current` placed in the app directory. -The `current` symbolic link always points to the latest release of our app: - -```php -... - -@task('update_symlinks') - echo "Linking storage directory" - rm -rf {{ $new_release_dir }}/storage - ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage - - echo 'Linking .env file' - ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env - - echo 'Linking current release' - ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current -@endtask -``` - -As you see, we use `-nfs` as an option for `ln` command, which says that the `storage`, `.env` and `current` no longer points to the preview's release and will point them to the new release by force (`f` from `-nfs` means force), which is the case when we are doing multiple deployments. - -### Full script - -The script is ready, but make sure to change the `deployer@192.168.1.1` to your server and also change `/var/www/app` with the directory you want to deploy your app. - -At the end, our `Envoy.blade.php` file will look like this: - -```php -@servers(['web' => 'deployer@192.168.1.1']) - -@setup - $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git'; - $releases_dir = '/var/www/app/releases'; - $app_dir = '/var/www/app'; - $release = date('YmdHis'); - $new_release_dir = $releases_dir .'/'. $release; -@endsetup - -@story('deploy') - clone_repository - run_composer - update_symlinks -@endstory - -@task('clone_repository') - echo 'Cloning repository' - [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} - git clone --depth 1 {{ $repository }} {{ $new_release_dir }} -@endtask - -@task('run_composer') - echo "Starting deployment ({{ $release }})" - cd {{ $new_release_dir }} - composer install --prefer-dist --no-scripts -q -o -@endtask - -@task('update_symlinks') - echo "Linking storage directory" - rm -rf {{ $new_release_dir }}/storage - ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage - - echo 'Linking .env file' - ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env - - echo 'Linking current release' - ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current -@endtask -``` - -One more thing we should do before any deployment is to manually copy our application `storage` folder to the `/var/www/app` directory on the server for the first time. -You might want to create another Envoy task to do that for you. -We also create the `.env` file in the same path to setup our production environment variables for Laravel. -These are persistent data and will be shared to every new release. - -Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../ci/environments.md), which will be described [later](#setting-up-gitlab-ci-cd) in this tutorial. - -Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch. -To keep things simple, we commit directly to `master`, without using [feature-branches](../../workflow/gitlab_flow.md/#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial. -In a real world project, teams may use [Issue Tracker](../../user/project/issues/index.md) and [Merge Requests](../../user/project/merge_requests/index.md) to move their code across branches: - -```bash -git add Envoy.blade.php -git commit -m 'Add Envoy' -git push origin master -``` - -## Continuous Integration with GitLab - -We have our app ready on GitLab, and we also can deploy it manually. -But let's take a step forward to do it automatically with [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) method. -We need to check every commit with a set of automated tests to become aware of issues at the earliest, and then, we can deploy to the target environment if we are happy with the result of the tests. - -[GitLab CI/CD](../../ci/README.md) allows us to use [Docker](https://docker.com/) engine to handle the process of testing and deploying our app. -In the case you're not familiar with Docker, refer to [How to Automate Docker Deployments](http://paislee.io/how-to-automate-docker-deployments/). - -To be able to build, test, and deploy our app with GitLab CI/CD, we need to prepare our work environment. -To do that, we'll use a Docker image which has the minimum requirements that a Laravel app needs to run. -[There are other ways](../../ci/examples/php.md/#test-php-projects-using-the-docker-executor) to do that as well, but they may lead our builds run slowly, which is not what we want when there are faster options to use. - -With Docker images our builds run incredibly faster! - -### Create a Container Image - -Let's create a [Dockerfile](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Dockerfile) in the root directory of our app with the following content: - -```bash -# Set the base image for subsequent instructions -FROM php:7.1 - -# Update packages -RUN apt-get update - -# Install PHP and composer dependencies -RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev - -# Clear out the local repository of retrieved package files -RUN apt-get clean - -# Install needed extensions -# Here you can install any other extension that you need during the test and deployment process -RUN docker-php-ext-install mcrypt pdo_mysql zip - -# Install Composer -RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - -# Install Laravel Envoy -RUN composer global require "laravel/envoy=~1.0" -``` - -We added the [official PHP 7.1 Docker image](https://hub.docker.com/r/_/php/), which consist of a minimum installation of Debian Jessie with PHP pre-installed, and works perfectly for our use case. - -We used `docker-php-ext-install` (provided by the official PHP Docker image) to install the PHP extensions we need. - -#### Setting Up GitLab Container Registry - -Now that we have our `Dockerfile` let's build and push it to our [GitLab Container Registry](../../user/project/container_registry.md). - -> The registry is the place to store and tag images for later use. Developers may want to maintain their own registry for private, company images, or for throw-away images used only in testing. Using GitLab Container Registry means you don't need to set up and administer yet another service or use a public registry. - -On your GitLab project repository navigate to the **Registry** tab. - -![container registry page empty image](img/container_registry_page_empty_image.png) - -You may need to [enable Container Registry](../../user/project/container_registry.md#enable-the-container-registry-for-your-project) to your project to see this tab. You'll find it under your project's **Settings > General > Sharing and permissions**. - -![container registry checkbox](img/container_registry_checkbox.png) - -To start using Container Registry on our machine, we first need to login to the GitLab registry using our GitLab username and password: - -```bash -docker login registry.gitlab.com -``` -Then we can build and push our image to GitLab: - -```bash -docker build -t registry.gitlab.com/<USERNAME>/laravel-sample . - -docker push registry.gitlab.com/<USERNAME>/laravel-sample -``` - ->**Note:** -To run the above commands, we first need to have [Docker](https://docs.docker.com/engine/installation/) installed on our machine. - -Congratulations! You just pushed the first Docker image to the GitLab Registry, and if you refresh the page you should be able to see it: - -![container registry page with image](img/container_registry_page_with_image.jpg) - ->**Note:** -You can also [use GitLab CI/CD](https://about.gitlab.com/2016/05/23/gitlab-container-registry/#use-with-gitlab-ci) to build and push your Docker images, rather than doing that on your machine. - -We'll use this image further down in the `.gitlab-ci.yml` configuration file to handle the process of testing and deploying our app. - -Let's commit the `Dockerfile` file. - -```bash -git add Dockerfile -git commit -m 'Add Dockerfile' -git push origin master -``` - -### Setting up GitLab CI/CD - -In order to build and test our app with GitLab CI/CD, we need a file called `.gitlab-ci.yml` in our repository's root. It is similar to Circle CI and Travis CI, but built-in GitLab. - -Our `.gitlab-ci.yml` file will look like this: - -```yaml -image: registry.gitlab.com/<USERNAME>/laravel-sample:latest - -services: - - mysql:5.7 - -variables: - MYSQL_DATABASE: homestead - MYSQL_ROOT_PASSWORD: secret - DB_HOST: mysql - DB_USERNAME: root - -stages: - - test - - deploy - -unit_test: - stage: test - script: - - cp .env.example .env - - composer install - - php artisan key:generate - - php artisan migrate - - vendor/bin/phpunit - -deploy_production: - stage: deploy - script: - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - - eval $(ssh-agent -s) - - ssh-add <(echo "$SSH_PRIVATE_KEY") - - mkdir -p ~/.ssh - - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - - - ~/.composer/vendor/bin/envoy run deploy - environment: - name: production - url: http://192.168.1.1 - when: manual - only: - - master -``` - -That's a lot to take in, isn't it? Let's run through it step by step. - -#### Image and Services - -[GitLab Runners](../../ci/runners/README.md) run the script defined by `.gitlab-ci.yml`. -The `image` keyword tells the Runners which image to use. -The `services` keyword defines additional images [that are linked to the main image](../../ci/docker/using_docker_images.md/#what-is-a-service). -Here we use the container image we created before as our main image and also use MySQL 5.7 as a service. - -```yaml -image: registry.gitlab.com/<USERNAME>/laravel-sample:latest - -services: - - mysql:5.7 - -... -``` - ->**Note:** -If you wish to test your app with different PHP versions and [database management systems](../../ci/services/README.md), you can define different `image` and `services` keywords for each test job. - -#### Variables - -GitLab CI/CD allows us to use [environment variables](../../ci/yaml/README.md#variables) in our jobs. -We defined MySQL as our database management system, which comes with a superuser root created by default. - -So we should adjust the configuration of MySQL instance by defining `MYSQL_DATABASE` variable as our database name and `MYSQL_ROOT_PASSWORD` variable as the password of `root`. -Find out more about MySQL variables at the [official MySQL Docker Image](https://hub.docker.com/r/_/mysql/). - -Also set the variables `DB_HOST` to `mysql` and `DB_USERNAME` to `root`, which are Laravel specific variables. -We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../ci/docker/using_docker_images.md/#how-services-are-linked-to-the-build). - -```yaml -... - -variables: - MYSQL_DATABASE: homestead - MYSQL_ROOT_PASSWORD: secret - DB_HOST: mysql - DB_USERNAME: root - -... -``` - -#### Unit Test as the first job - -We defined the required shell scripts as an array of the [script](../../ci/yaml/README.md#script) variable to be executed when running `unit_test` job. - -These scripts are some Artisan commands to prepare the Laravel, and, at the end of the script, we'll run the tests by `PHPUnit`. - -```yaml -... - -unit_test: - script: - # Install app dependencies - - composer install - # Setup .env - - cp .env.example .env - # Generate an environment key - - php artisan key:generate - # Run migrations - - php artisan migrate - # Run tests - - vendor/bin/phpunit - -... -``` - -#### Deploy to production - -The job `deploy_production` will deploy the app to the production server. -To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ci/ssh_keys/README.md/#ssh-keys-when-using-the-docker-executor). -If the SSH keys have added successfully, we can run Envoy. - -As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well. -The [environment](../../ci/yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment. -The `url` keyword is used to generate a link to our application on the GitLab Environments page. -The `only` keyword tells GitLab CI that the job should be executed only when the pipeline is building the `master` branch. -Lastly, `when: manual` is used to turn the job from running automatically to a manual action. - -```yaml -... - -deploy_production: - script: - # Add the private SSH key to the build environment - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - - eval $(ssh-agent -s) - - ssh-add <(echo "$SSH_PRIVATE_KEY") - - mkdir -p ~/.ssh - - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - - # Run Envoy - - ~/.composer/vendor/bin/envoy run deploy - - environment: - name: production - url: http://192.168.1.1 - when: manual - only: - - master -``` - -You may also want to add another job for [staging environment](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments), to final test your application before deploying to production. - -### Turn on GitLab CI/CD - -We have prepared everything we need to test and deploy our app with GitLab CI/CD. -To do that, commit and push `.gitlab-ci.yml` to the `master` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**. - -![pipelines page](img/pipelines_page.png) - -Here we see our **Test** and **Deploy** stages. -The **Test** stage has the `unit_test` build running. -click on it to see the Runner's output. - -![pipeline page](img/pipeline_page.png) - -After our code passed through the pipeline successfully, we can deploy to our production server by clicking the **play** button on the right side. - -![pipelines page deploy button](img/pipelines_page_deploy_button.png) - -Once the deploy pipeline passed successfully, navigate to **Pipelines > Environments**. - -![environments page](img/environments_page.png) - -If something doesn't work as expected, you can roll back to the latest working version of your app. - -![environment page](img/environment_page.png) - -By clicking on the external link icon specified on the right side, GitLab opens the production website. -Our deployment successfully was done and we can see the application is live. - -![laravel welcome page](img/laravel_welcome_page.png) - -In the case that you're interested to know how is the application directory structure on the production server after deployment, here are three directories named `current`, `releases` and `storage`. -As you know, the `current` directory is a symbolic link that points to the latest release. -The `.env` file consists of our Laravel environment variables. - -![production server app directory](img/production_server_app_directory.png) - -If you navigate to the `current` directory, you should see the application's content. -As you see, the `.env` is pointing to the `/var/www/app/.env` file and also `storage` is pointing to the `/var/www/app/storage/` directory. - -![production server current directory](img/production_server_current_directory.png) - -## Conclusion - -We configured GitLab CI to perform automated tests and used the method of [Continuous Delivery](https://continuousdelivery.com/) to deploy to production a Laravel application with Envoy, directly from the codebase. - -Envoy also was a great match to help us deploy the application without writing our custom bash script and doing Linux magics. +This document was moved to [another location](../../ci/examples/laravel_with_gitlab_and_envoy/index.md). diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md index c0bbcfe2a8a..b7594cfef7f 100644 --- a/doc/articles/openshift_and_gitlab/index.md +++ b/doc/articles/openshift_and_gitlab/index.md @@ -1,510 +1 @@ -# Getting started with OpenShift Origin 3 and GitLab - -> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) || -> **Publication date:** 2016-06-28 - -## Introduction - -[OpenShift Origin][openshift] is an open source container application -platform created by [RedHat], based on [kubernetes] and [Docker]. That means -you can host your own PaaS for free and almost with no hassle. - -In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's -official Docker image while getting familiar with the web interface and CLI -tools that will help us achieve our goal. - ---- - -## Prerequisites - -OpenShift 3 is not yet deployed on RedHat's offered Online platform ([openshift.com]), -so in order to test it, we will use an [all-in-one Virtualbox image][vm] that is -offered by the OpenShift developers and managed by Vagrant. If you haven't done -already, go ahead and install the following components as they are essential to -test OpenShift easily: - -- [VirtualBox] -- [Vagrant] -- [OpenShift Client][oc] (`oc` for short) - -It is also important to mention that for the purposes of this tutorial, the -latest Origin release is used: - -- **oc** `v1.3.0` (must be [installed][oc-gh] locally on your computer) -- **openshift** `v1.3.0` (is pre-installed in the [VM image][vm-new]) -- **kubernetes** `v1.3.0` (is pre-installed in the [VM image][vm-new]) - ->**Note:** -If you intend to deploy GitLab on a production OpenShift cluster, there are some -limitations to bare in mind. Read on the [limitations](#current-limitations) -section for more information and follow the linked links for the relevant -discussions. - -Now that you have all batteries, let's see how easy it is to test OpenShift -on your computer. - -## Getting familiar with OpenShift Origin - -The environment we are about to use is based on CentOS 7 which comes with all -the tools needed pre-installed: Docker, kubernetes, OpenShift, etcd. - -### Test OpenShift using Vagrant - -As of this writing, the all-in-one VM is at version 1.3, and that's -what we will use in this tutorial. - -In short: - -1. Open a terminal and in a new directory run: - ```sh - vagrant init openshift/origin-all-in-one - ``` -1. This will generate a Vagrantfile based on the all-in-one VM image -1. In the same directory where you generated the Vagrantfile - enter: - - ```sh - vagrant up - ``` - -This will download the VirtualBox image and fire up the VM with some preconfigured -values as you can see in the Vagrantfile. As you may have noticed, you need -plenty of RAM (5GB in our example), so make sure you have enough. - -Now that OpenShift is setup, let's see how the web console looks like. - -### Explore the OpenShift web console - -Once Vagrant finishes its thing with the VM, you will be presented with a -message which has some important information. One of them is the IP address -of the deployed OpenShift platform and in particular <https://10.2.2.2:8443/console/>. -Open this link with your browser and accept the self-signed certificate in -order to proceed. - -Let's login as admin with username/password `admin/admin`. This is what the -landing page looks like: - -![openshift web console](img/web-console.png) - -You can see that a number of [projects] are already created for testing purposes. - -If you head over the `openshift-infra` project, a number of services with their -respective pods are there to explore. - -![openshift web console](img/openshift-infra-project.png) - -We are not going to explore the whole interface, but if you want to learn about -the key concepts of OpenShift, read the [core concepts reference][core] in the -official documentation. - -### Explore the OpenShift CLI - -OpenShift Client (`oc`), is a powerful CLI tool that talks to the OpenShift API -and performs pretty much everything you can do from the web UI and much more. - -Assuming you have [installed][oc] it, let's explore some of its main -functionalities. - -Let's first see the version of `oc`: - -```sh -$ oc version - -oc v1.3.0 -kubernetes v1.3.0+52492b4 -``` - -With `oc help` you can see the top level arguments you can run with `oc` and -interact with your cluster, kubernetes, run applications, create projects and -much more. - -Let's login to the all-in-one VM and see how to achieve the same results like -when we visited the web console earlier. The username/password for the -administrator user is `admin/admin`. There is also a test user with username/ -password `user/user`, with limited access. Let's login as admin for the moment: - -```sh -$ oc login https://10.2.2.2:8443 - -Authentication required for https://10.2.2.2:8443 (openshift) -Username: admin -Password: -Login successful. - -You have access to the following projects and can switch between them with 'oc project <projectname>': - - * cockpit - * default (current) - * delete - * openshift - * openshift-infra - * sample - -Using project "default". -``` - -Switch to the `openshift-infra` project with: - -```sh -oc project openshift-infra -``` - -And finally, see its status: - -```sh -oc status -``` - -The last command should spit a bunch of information about the statuses of the -pods and the services, which if you look closely is what we encountered in the -second image when we explored the web console. - -You can always read more about `oc` in the [OpenShift CLI documentation][oc]. - -### Troubleshooting the all-in-one VM - -Using the all-in-one VM gives you the ability to test OpenShift whenever you -want. That means you get to play with it, shutdown the VM, and pick up where -you left off. - -Sometimes though, you may encounter some issues, like OpenShift not running -when booting up the VM. The web UI may not responding or you may see issues -when trying to login with `oc`, like: - -``` -The connection to the server 10.2.2.2:8443 was refused - did you specify the right host or port? -``` - -In that case, the OpenShift service might not be running, so in order to fix it: - -1. SSH into the VM by going to the directory where the Vagrantfile is and then - run: - - ```sh - vagrant ssh - ``` - -1. Run `systemctl` and verify by the output that the `openshift` service is not - running (it will be in red color). If that's the case start the service with: - - ```sh - sudo systemctl start openshift - ``` - -1. Verify the service is up with: - - ```sh - systemctl status openshift -l - ``` - -Now you will be able to login using `oc` (like we did before) and visit the web -console. - -## Deploy GitLab - -Now that you got a taste of what OpenShift looks like, let's deploy GitLab! - -### Create a new project - -First, we will create a new project to host our application. You can do this -either by running the CLI client: - -```bash -$ oc new-project gitlab -``` - -or by using the web interface: - -![Create a new project from the UI](img/create-project-ui.png) - -If you used the command line, `oc` automatically uses the new project and you -can see its status with: - -```sh -$ oc status - -In project gitlab on server https://10.2.2.2:8443 - -You have no services, deployment configs, or build configs. -Run 'oc new-app' to create an application. -``` - -If you visit the web console, you can now see `gitlab` listed in the projects list. - -The next step is to import the OpenShift template for GitLab. - -### Import the template - -The [template][templates] is basically a JSON file which describes a set of -related object definitions to be created together, as well as a set of -parameters for those objects. - -The template for GitLab resides in the Omnibus GitLab repository under the -docker directory. Let's download it locally with `wget`: - -```bash -wget https://gitlab.com/gitlab-org/omnibus-gitlab/raw/master/docker/openshift-template.json -``` - -And then let's import it in OpenShift: - -```bash -oc create -f openshift-template.json -n openshift -``` - ->**Note:** -The `-n openshift` namespace flag is a trick to make the template available to all -projects. If you recall from when we created the `gitlab` project, `oc` switched -to it automatically, and that can be verified by the `oc status` command. If -you omit the namespace flag, the application will be available only to the -current project, in our case `gitlab`. The `openshift` namespace is a global -one that the administrators should use if they want the application to be -available to all users. - -We are now ready to finally deploy GitLab! - -### Create a new application - -The next step is to use the template we previously imported. Head over to the -`gitlab` project and hit the **Add to Project** button. - -![Add to project](img/add-to-project.png) - -This will bring you to the catalog where you can find all the pre-defined -applications ready to deploy with the click of a button. Search for `gitlab` -and you will see the previously imported template: - -![Add GitLab to project](img/add-gitlab-to-project.png) - -Select it, and in the following screen you will be presented with the predefined -values used with the GitLab template: - -![GitLab settings](img/gitlab-settings.png) - -Notice at the top that there are three resources to be created with this -template: - -- `gitlab-ce` -- `gitlab-ce-redis` -- `gitlab-ce-postgresql` - -While PostgreSQL and Redis are bundled in Omnibus GitLab, the template is using -separate images as you can see from [this line][line] in the template. - -The predefined values have been calculated for the purposes of testing out -GitLab in the all-in-one VM. You don't need to change anything here, hit -**Create** to start the deployment. - -If you are deploying to production you will want to change the **GitLab instance -hostname** and use greater values for the volume sizes. If you don't provide a -password for PostgreSQL, it will be created automatically. - ->**Note:** -The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will -resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a -trick to have distinct FQDNs pointing to services that are on our local network. -Read more on how this works in <http://xip.io>. - -Now that we configured this, let's see how to manage and scale GitLab. - -## Manage and scale GitLab - -Setting up GitLab for the first time might take a while depending on your -internet connection and the resources you have attached to the all-in-one VM. -GitLab's docker image is quite big (~500MB), so you'll have to wait until -it's downloaded and configured before you use it. - -### Watch while GitLab gets deployed - -Navigate to the `gitlab` project at **Overview**. You can notice that the -deployment is in progress by the orange color. The Docker images are being -downloaded and soon they will be up and running. - -![GitLab overview](img/gitlab-overview.png) - -Switch to the **Browse > Pods** and you will eventually see all 3 pods in a -running status. Remember the 3 resources that were to be created when we first -created the GitLab app? This is where you can see them in action. - -![Running pods](img/running-pods.png) - -You can see GitLab being reconfigured by taking look at the logs in realtime. -Click on `gitlab-ce-2-j7ioe` (your ID will be different) and go to the **Logs** -tab. - -![GitLab logs](img/gitlab-logs.png) - -At a point you should see a _**gitlab Reconfigured!**_ message in the logs. -Navigate back to the **Overview** and hopefully all pods will be up and running. - -![GitLab running](img/gitlab-running.png) - -Congratulations! You can now navigate to your new shinny GitLab instance by -visiting <http://gitlab.apps.10.2.2.2.xip.io> where you will be asked to -change the root user password. Login using `root` as username and providing the -password you just set, and start using GitLab! - -### Scale GitLab with the push of a button - -If you reach to a point where your GitLab instance could benefit from a boost -of resources, you'd be happy to know that you can scale up with the push of a -button. - -In the **Overview** page just click the up arrow button in the pod where -GitLab is. The change is instant and you can see the number of [replicas] now -running scaled to 2. - -![GitLab scale](img/gitlab-scale.png) - -Upping the GitLab pods is actually like adding new application servers to your -cluster. You can see how that would work if you didn't use GitLab with -OpenShift by following the [HA documentation][ha] for the application servers. - -Bare in mind that you may need more resources (CPU, RAM, disk space) when you -scale up. If a pod is in pending state for too long, you can navigate to -**Browse > Events** and see the reason and message of the state. - -![No resources](img/no-resources.png) - -### Scale GitLab using the `oc` CLI - -Using `oc` is super easy to scale up the replicas of a pod. You may want to -skim through the [basic CLI operations][basic-cli] to get a taste how the CLI -commands are used. Pay extra attention to the object types as we will use some -of them and their abbreviated versions below. - -In order to scale up, we need to find out the name of the replication controller. -Let's see how to do that using the following steps. - -1. Make sure you are in the `gitlab` project: - - ```sh - oc project gitlab - ``` - -1. See what services are used for this project: - - ```sh - oc get svc - ``` - - The output will be similar to: - - ``` - NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE - gitlab-ce 172.30.243.177 <none> 22/TCP,80/TCP 5d - gitlab-ce-postgresql 172.30.116.75 <none> 5432/TCP 5d - gitlab-ce-redis 172.30.105.88 <none> 6379/TCP 5d - ``` - -1. We need to see the replication controllers of the `gitlab-ce` service. - Get a detailed view of the current ones: - - ```sh - oc describe rc gitlab-ce - ``` - - This will return a large detailed list of the current replication controllers. - Search for the name of the GitLab controller, usually `gitlab-ce-1` or if - that failed at some point and you spawned another one, it will be named - `gitlab-ce-2`. - -1. Scale GitLab using the previous information: - - ```sh - oc scale --replicas=2 replicationcontrollers gitlab-ce-2 - ``` - -1. Get the new replicas number to make sure scaling worked: - - ```sh - oc get rc gitlab-ce-2 - ``` - - which will return something like: - - ``` - NAME DESIRED CURRENT AGE - gitlab-ce-2 2 2 5d - ``` - -And that's it! We successfully scaled the replicas to 2 using the CLI. - -As always, you can find the name of the controller using the web console. Just -click on the service you are interested in and you will see the details in the -right sidebar. - -![Replication controller name](img/rc-name.png) - -### Autoscaling GitLab - -In case you were wondering whether there is an option to autoscale a pod based -on the resources of your server, the answer is yes, of course there is. - -We will not expand on this matter, but feel free to read the documentation on -OpenShift's website about [autoscaling]. - -## Current limitations - -As stated in the [all-in-one VM][vm] page: - -> By default, OpenShift will not allow a container to run as root or even a -non-random container assigned userid. Most Docker images in the Dockerhub do not -follow this best practice and instead run as root. - -The all-in-one VM we are using has this security turned off so it will not -bother us. In any case, it is something to keep in mind when deploying GitLab -on a production cluster. - -In order to deploy GitLab on a production cluster, you will need to assign the -GitLab service account to the `anyuid` Security Context. - -1. Edit the Security Context: - ```sh - oc edit scc anyuid - ``` - -1. Add `system:serviceaccount:<project>:gitlab-ce-user` to the `users` section. - If you changed the Application Name from the default the user will - will be `<app-name>-user` instead of `gitlab-ce-user` - -1. Save and exit the editor - -## Conclusion - -By now, you should have an understanding of the basic OpenShift Origin concepts -and a sense of how things work using the web console or the CLI. - -GitLab was hard to install in previous versions of OpenShift, -but now that belongs to the past. Upload a template, create a project, add an -application and you are done. You are ready to login to your new GitLab instance. - -And remember that in this tutorial we just scratched the surface of what Origin -is capable of. As always, you can refer to the detailed -[documentation][openshift-docs] to learn more about deploying your own OpenShift -PaaS and managing your applications with the ease of containers. - -[RedHat]: https://www.redhat.com/en "RedHat website" -[openshift]: https://www.openshift.org "OpenShift Origin website" -[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM" -[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas" -[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab" -[openshift.com]: https://openshift.com "OpenShift Online" -[kubernetes]: http://kubernetes.io/ "Kubernetes website" -[Docker]: https://www.docker.com "Docker website" -[oc]: https://docs.openshift.org/latest/cli_reference/get_started_cli.html "Documentation - oc CLI documentation" -[VirtualBox]: https://www.virtualbox.org/wiki/Downloads "VirtualBox downloads" -[Vagrant]: https://www.vagrantup.com/downloads.html "Vagrant downloads" -[projects]: https://docs.openshift.org/latest/dev_guide/projects.html "Documentation - Projects overview" -[core]: https://docs.openshift.org/latest/architecture/core_concepts/index.html "Documentation - Core concepts of OpenShift Origin" -[templates]: https://docs.openshift.org/latest/architecture/core_concepts/templates.html "Documentation - OpenShift templates" -[old-post]: https://blog.openshift.com/deploy-gitlab-openshift/ "Old post - Deploy GitLab on OpenShift" -[line]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239 "GitLab - OpenShift template" -[oc-gh]: https://github.com/openshift/origin/releases/tag/v1.3.0 "Openshift 1.3.0 release on GitHub" -[ha]: http://docs.gitlab.com/ce/administration/high_availability/gitlab.html "Documentation - GitLab High Availability" -[replicas]: https://docs.openshift.org/latest/architecture/core_concepts/deployments.html#replication-controllers "Documentation - Replication controller" -[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale" -[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations" -[openshift-docs]: https://docs.openshift.org "OpenShift documentation" +This document was moved to [another location](../../install/openshift_and_gitlab/index.html). diff --git a/doc/ci/README.md b/doc/ci/README.md index 05d792dea0f..5829aaee9c9 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -120,7 +120,7 @@ Here is an collection of tutorials and guides on setting up your CI pipeline. - [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md) - [Analyze code quality with the Code Climate CLI](examples/code_climate.md) - **Articles** - - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](../articles/laravel_with_gitlab_and_envoy/index.md) + - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](examples/laravel_with_gitlab_and_envoy/index.md) - [How to deploy Maven projects to Artifactory with GitLab CI/CD](examples/artifactory_and_gitlab/index.md) - [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 25a0c5dcff5..d4590d0f495 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -16,6 +16,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo - [Testing a PHP application](php.md) - [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md) +- [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) ### Ruby diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png Binary files differindex a56c07a0da7..a56c07a0da7 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png Binary files differindex b1406fed6b8..b1406fed6b8 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg Binary files differindex d1f0cbc08ab..d1f0cbc08ab 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png Binary files differindex 9aae11b8679..9aae11b8679 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/environment_page.png Binary files differindex a06b6d417cd..a06b6d417cd 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/environment_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/environments_page.png Binary files differindex d357ecda7d2..d357ecda7d2 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/environments_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png Binary files differindex 3bb21fd12b4..3bb21fd12b4 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png Binary files differindex bc188f83fb1..bc188f83fb1 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipeline_page.png Binary files differindex baf8dec499c..baf8dec499c 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipeline_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipelines_page.png Binary files differindex d96c43bcf16..d96c43bcf16 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipelines_page.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png Binary files differindex 997db10189f..997db10189f 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png Binary files differindex 6dbc29fc25c..6dbc29fc25c 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png Binary files differindex 8a6dcccfa38..8a6dcccfa38 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.png Binary files differindex 658c0b5bcac..658c0b5bcac 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.png +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/secret_variables_page.png diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md new file mode 100644 index 00000000000..e1aff6fdf36 --- /dev/null +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -0,0 +1,684 @@ +--- +redirect_from: 'https://docs.gitlab.com/ee/articles/laravel_with_gitlab_and_envoy/index.html' +--- + +# Test and deploy Laravel applications with GitLab CI/CD and Envoy + +> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Mehran Rasulian](https://gitlab.com/mehranrasulian) || +> **Publication date:** 2017-08-31 + +## Introduction + +GitLab features our applications with Continuous Integration, and it is possible to easily deploy the new code changes to the production server whenever we want. + +In this tutorial, we'll show you how to initialize a [Laravel](http://laravel.com/) application and setup our [Envoy](https://laravel.com/docs/envoy) tasks, then we'll jump into see how to test and deploy it with [GitLab CI/CD](../README.md) via [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/). + +We assume you have a basic experience with Laravel, Linux servers, +and you know how to use GitLab. + +Laravel is a high quality web framework written in PHP. +It has a great community with a [fantastic documentation](https://laravel.com/docs). +Aside from the usual routing, controllers, requests, responses, views, and (blade) templates, out of the box Laravel provides plenty of additional services such as cache, events, localization, authentication and many others. + +We will use [Envoy](https://laravel.com/docs/master/envoy) as an SSH task runner based on PHP. +It uses a clean, minimal [Blade syntax](https://laravel.com/docs/blade) to setup tasks that can run on remote servers, such as, cloning your project from the repository, installing the Composer dependencies, and running [Artisan commands](https://laravel.com/docs/artisan). + +## Initialize our Laravel app on GitLab + +We assume [you have installed a new laravel project](https://laravel.com/docs/installation#installation), so let's start with a unit test, and initialize Git for the project. + +### Unit Test + +Every new installation of Laravel (currently 5.4) comes with two type of tests, 'Feature' and 'Unit', placed in the tests directory. +Here's a unit test from `test/Unit/ExampleTest.php`: + +```php +<?php + +namespace Tests\Unit; + +... + +class ExampleTest extends TestCase +{ + public function testBasicTest() + { + $this->assertTrue(true); + } +} +``` + +This test is as simple as asserting that the given value is true. + +Laravel uses `PHPUnit` for tests by default. +If we run `vendor/bin/phpunit` we should see the green output: + +```bash +vendor/bin/phpunit +OK (1 test, 1 assertions) +``` + +This test will be used later for continuously testing our app with GitLab CI/CD. + +### Push to GitLab + +Since we have our app up and running locally, it's time to push the codebase to our remote repository. +Let's create [a new project](../../../gitlab-basics/create-project.md) in GitLab named `laravel-sample`. +After that, follow the command line instructions displayed on the project's homepage to initiate the repository on our machine and push the first commit. + + +```bash +cd laravel-sample +git init +git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git +git add . +git commit -m 'Initial Commit' +git push -u origin master +``` + +## Configure the production server + +Before we begin setting up Envoy and GitLab CI/CD, let's quickly make sure the production server is ready for deployment. +We have installed LEMP stack which stands for Linux, Nginx, MySQL and PHP on our Ubuntu 16.04. + +### Create a new user + +Let's now create a new user that will be used to deploy our website and give it +the needed permissions using [Linux ACL](https://serversforhackers.com/video/linux-acls): + +```bash +# Create user deployer +sudo adduser deployer +# Give the read-write-execute permissions to deployer user for directory /var/www +sudo setfacl -R -m u:deployer:rwx /var/www +``` + +If you don't have ACL installed on your Ubuntu server, use this command to install it: + +```bash +sudo apt install acl +``` + +### Add SSH key + +Let's suppose we want to deploy our app to the production server from a private repository on GitLab. First, we need to [generate a new SSH key pair **with no passphrase**](../../../ssh/README.md) for the deployer user. + +After that, we need to copy the private key, which will be used to connect to our server as the deployer user with SSH, to be able to automate our deployment process: + +```bash +# As the deployer user on server +# +# Copy the content of public key to authorized_keys +cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys +# Copy the private key text block +cat ~/.ssh/id_rsa +``` + +Now, let's add it to your GitLab project as a [secret variable](../../variables/README.md#secret-variables). +Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. +They can be added per project by navigating to the project's **Settings** > **CI/CD**. + +![secret variables page](img/secret_variables_page.png) + +To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier. +We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password. + +We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../../ssh/README.md/#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../../gitlab-basics/command-line-commands.md/#start-working-on-your-project). + + +```bash +# As the deployer user on the server +# +# Copy the public key +cat ~/.ssh/id_rsa.pub +``` + +![deploy keys page](img/deploy_keys_page.png) + +To the field **Title**, add any name you want, and paste the public key into the **Key** field. + +Now, let's clone our repository on the server just to make sure the `deployer` user has access to the repository. + +```bash +# As the deployer user on server +# +git clone git@gitlab.example.com:<USERNAME>/laravel-sample.git +``` + +>**Note:** +Answer **yes** if asked `Are you sure you want to continue connecting (yes/no)?`. +It adds GitLab.com to the known hosts. + +### Configuring Nginx + +Now, let's make sure our web server configuration points to the `current/public` rather than `public`. + +Open the default Nginx server block configuration file by typing: + +```bash +sudo nano /etc/nginx/sites-available/default +``` + +The configuration should be like this. + +``` +server { + root /var/www/app/current/public; + server_name example.com; + # Rest of the configuration +} +``` + +>**Note:** +You may replace the app's name in `/var/www/app/current/public` with the folder name of your application. + +## Setting up Envoy + +So we have our Laravel app ready for production. +The next thing is to use Envoy to perform the deploy. + +To use Envoy, we should first install it on our local machine [using the given instructions by Laravel](https://laravel.com/docs/envoy/#introduction). + +### How Envoy works + +The pros of Envoy is that it doesn't require Blade engine, it just uses Blade syntax to define tasks. +To start, we create an `Envoy.blade.php` in the root of our app with a simple task to test Envoy. + + +```php +@servers(['web' => 'remote_username@remote_host']) + +@task('list', [on => 'web']) + ls -l +@endtask +``` + +As you may expect, we have an array within `@servers` directive at the top of the file, which contains a key named `web` with a value of the server's address (e.g. `deployer@192.168.1.1`). +Then within our `@task` directive we define the bash commands that should be run on the server when the task is executed. + +On the local machine use the `run` command to run Envoy tasks. + +```bash +envoy run list +``` + +It should execute the `list` task we defined earlier, which connects to the server and lists directory contents. + +Envoy is not a dependency of Laravel, therefore you can use it for any PHP application. + +### Zero downtime deployment + +Every time we deploy to the production server, Envoy downloads the latest release of our app from GitLab repository and replace it with preview's release. +Envoy does this without any [downtime](https://en.wikipedia.org/wiki/Downtime), +so we don't have to worry during the deployment while someone might be reviewing the site. +Our deployment plan is to clone the latest release from GitLab repository, install the Composer dependencies and finally, activate the new release. + +#### @setup directive + +The first step of our deployment process is to define a set of variables within [@setup](https://laravel.com/docs/envoy/#setup) directive. +You may change the `app` to your application's name: + + +```php +... + +@setup + $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git'; + $releases_dir = '/var/www/app/releases'; + $app_dir = '/var/www/app'; + $release = date('YmdHis'); + $new_release_dir = $releases_dir .'/'. $release; +@endsetup + +... +``` + +- `$repository` is the address of our repository +- `$releases_dir` directory is where we deploy the app +- `$app_dir` is the actual location of the app that is live on the server +- `$release` contains a date, so every time that we deploy a new release of our app, we get a new folder with the current date as name +- `$new_release_dir` is the full path of the new release which is used just to make the tasks cleaner + +#### @story directive + +The [@story](https://laravel.com/docs/envoy/#stories) directive allows us define a list of tasks that can be run as a single task. +Here we have three tasks called `clone_repository`, `run_composer`, `update_symlinks`. These variables are usable to making our task's codes more cleaner: + + +```php +... + +@story('deploy') + clone_repository + run_composer + update_symlinks +@endstory + +... +``` + +Let's create these three tasks one by one. + +#### Clone the repository + +The first task will create the `releases` directory (if it doesn't exist), and then clone the `master` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable. +The `releases` directory will hold all our deployments: + +```php +... + +@task('clone_repository') + echo 'Cloning repository' + [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} + git clone --depth 1 {{ $repository }} {{ $new_release_dir }} +@endtask + +... +``` + +While our project grows, its Git history will be very very long over time. +Since we are creating a directory per release, it might not be necessary to have the history of the project downloaded for each release. +The `--depth 1` option is a great solution which saves systems time and disk space as well. + +#### Installing dependencies with Composer + +As you may know, this task just navigates to the new release directory and runs Composer to install the application dependencies: + +```php +... + +@task('run_composer') + echo "Starting deployment ({{ $release }})" + cd {{ $new_release_dir }} + composer install --prefer-dist --no-scripts -q -o +@endtask + +... +``` + +#### Activate new release + +Next thing to do after preparing the requirements of our new release, is to remove the storage directory from it and to create two symbolic links to point the application's `storage` directory and `.env` file to the new release. +Then, we need to create another symbolic link to the new release with the name of `current` placed in the app directory. +The `current` symbolic link always points to the latest release of our app: + +```php +... + +@task('update_symlinks') + echo "Linking storage directory" + rm -rf {{ $new_release_dir }}/storage + ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage + + echo 'Linking .env file' + ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env + + echo 'Linking current release' + ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current +@endtask +``` + +As you see, we use `-nfs` as an option for `ln` command, which says that the `storage`, `.env` and `current` no longer points to the preview's release and will point them to the new release by force (`f` from `-nfs` means force), which is the case when we are doing multiple deployments. + +### Full script + +The script is ready, but make sure to change the `deployer@192.168.1.1` to your server and also change `/var/www/app` with the directory you want to deploy your app. + +At the end, our `Envoy.blade.php` file will look like this: + +```php +@servers(['web' => 'deployer@192.168.1.1']) + +@setup + $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git'; + $releases_dir = '/var/www/app/releases'; + $app_dir = '/var/www/app'; + $release = date('YmdHis'); + $new_release_dir = $releases_dir .'/'. $release; +@endsetup + +@story('deploy') + clone_repository + run_composer + update_symlinks +@endstory + +@task('clone_repository') + echo 'Cloning repository' + [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }} + git clone --depth 1 {{ $repository }} {{ $new_release_dir }} +@endtask + +@task('run_composer') + echo "Starting deployment ({{ $release }})" + cd {{ $new_release_dir }} + composer install --prefer-dist --no-scripts -q -o +@endtask + +@task('update_symlinks') + echo "Linking storage directory" + rm -rf {{ $new_release_dir }}/storage + ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage + + echo 'Linking .env file' + ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env + + echo 'Linking current release' + ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current +@endtask +``` + +One more thing we should do before any deployment is to manually copy our application `storage` folder to the `/var/www/app` directory on the server for the first time. +You might want to create another Envoy task to do that for you. +We also create the `.env` file in the same path to setup our production environment variables for Laravel. +These are persistent data and will be shared to every new release. + +Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments.md), which will be described [later](#setting-up-gitlab-ci-cd) in this tutorial. + +Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch. +To keep things simple, we commit directly to `master`, without using [feature-branches](../../../workflow/gitlab_flow.md/#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial. +In a real world project, teams may use [Issue Tracker](../../../user/project/issues/index.md) and [Merge Requests](../../../user/project/merge_requests/index.md) to move their code across branches: + +```bash +git add Envoy.blade.php +git commit -m 'Add Envoy' +git push origin master +``` + +## Continuous Integration with GitLab + +We have our app ready on GitLab, and we also can deploy it manually. +But let's take a step forward to do it automatically with [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) method. +We need to check every commit with a set of automated tests to become aware of issues at the earliest, and then, we can deploy to the target environment if we are happy with the result of the tests. + +[GitLab CI/CD](../../README.md) allows us to use [Docker](https://docker.com/) engine to handle the process of testing and deploying our app. +In the case you're not familiar with Docker, refer to [How to Automate Docker Deployments](http://paislee.io/how-to-automate-docker-deployments/). + +To be able to build, test, and deploy our app with GitLab CI/CD, we need to prepare our work environment. +To do that, we'll use a Docker image which has the minimum requirements that a Laravel app needs to run. +[There are other ways](../php.md/#test-php-projects-using-the-docker-executor) to do that as well, but they may lead our builds run slowly, which is not what we want when there are faster options to use. + +With Docker images our builds run incredibly faster! + +### Create a Container Image + +Let's create a [Dockerfile](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Dockerfile) in the root directory of our app with the following content: + +```bash +# Set the base image for subsequent instructions +FROM php:7.1 + +# Update packages +RUN apt-get update + +# Install PHP and composer dependencies +RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev + +# Clear out the local repository of retrieved package files +RUN apt-get clean + +# Install needed extensions +# Here you can install any other extension that you need during the test and deployment process +RUN docker-php-ext-install mcrypt pdo_mysql zip + +# Install Composer +RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +# Install Laravel Envoy +RUN composer global require "laravel/envoy=~1.0" +``` + +We added the [official PHP 7.1 Docker image](https://hub.docker.com/r/_/php/), which consist of a minimum installation of Debian Jessie with PHP pre-installed, and works perfectly for our use case. + +We used `docker-php-ext-install` (provided by the official PHP Docker image) to install the PHP extensions we need. + +#### Setting Up GitLab Container Registry + +Now that we have our `Dockerfile` let's build and push it to our [GitLab Container Registry](../../../user/project/container_registry.md). + +> The registry is the place to store and tag images for later use. Developers may want to maintain their own registry for private, company images, or for throw-away images used only in testing. Using GitLab Container Registry means you don't need to set up and administer yet another service or use a public registry. + +On your GitLab project repository navigate to the **Registry** tab. + +![container registry page empty image](img/container_registry_page_empty_image.png) + +You may need to [enable Container Registry](../../../user/project/container_registry.md#enable-the-container-registry-for-your-project) to your project to see this tab. You'll find it under your project's **Settings > General > Sharing and permissions**. + +![container registry checkbox](img/container_registry_checkbox.png) + +To start using Container Registry on our machine, we first need to login to the GitLab registry using our GitLab username and password: + +```bash +docker login registry.gitlab.com +``` +Then we can build and push our image to GitLab: + +```bash +docker build -t registry.gitlab.com/<USERNAME>/laravel-sample . + +docker push registry.gitlab.com/<USERNAME>/laravel-sample +``` + +>**Note:** +To run the above commands, we first need to have [Docker](https://docs.docker.com/engine/installation/) installed on our machine. + +Congratulations! You just pushed the first Docker image to the GitLab Registry, and if you refresh the page you should be able to see it: + +![container registry page with image](img/container_registry_page_with_image.jpg) + +>**Note:** +You can also [use GitLab CI/CD](https://about.gitlab.com/2016/05/23/gitlab-container-registry/#use-with-gitlab-ci) to build and push your Docker images, rather than doing that on your machine. + +We'll use this image further down in the `.gitlab-ci.yml` configuration file to handle the process of testing and deploying our app. + +Let's commit the `Dockerfile` file. + +```bash +git add Dockerfile +git commit -m 'Add Dockerfile' +git push origin master +``` + +### Setting up GitLab CI/CD + +In order to build and test our app with GitLab CI/CD, we need a file called `.gitlab-ci.yml` in our repository's root. It is similar to Circle CI and Travis CI, but built-in GitLab. + +Our `.gitlab-ci.yml` file will look like this: + +```yaml +image: registry.gitlab.com/<USERNAME>/laravel-sample:latest + +services: + - mysql:5.7 + +variables: + MYSQL_DATABASE: homestead + MYSQL_ROOT_PASSWORD: secret + DB_HOST: mysql + DB_USERNAME: root + +stages: + - test + - deploy + +unit_test: + stage: test + script: + - cp .env.example .env + - composer install + - php artisan key:generate + - php artisan migrate + - vendor/bin/phpunit + +deploy_production: + stage: deploy + script: + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + - eval $(ssh-agent -s) + - ssh-add <(echo "$SSH_PRIVATE_KEY") + - mkdir -p ~/.ssh + - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + + - ~/.composer/vendor/bin/envoy run deploy + environment: + name: production + url: http://192.168.1.1 + when: manual + only: + - master +``` + +That's a lot to take in, isn't it? Let's run through it step by step. + +#### Image and Services + +[GitLab Runners](../../runners/README.md) run the script defined by `.gitlab-ci.yml`. +The `image` keyword tells the Runners which image to use. +The `services` keyword defines additional images [that are linked to the main image](../../docker/using_docker_images.md/#what-is-a-service). +Here we use the container image we created before as our main image and also use MySQL 5.7 as a service. + +```yaml +image: registry.gitlab.com/<USERNAME>/laravel-sample:latest + +services: + - mysql:5.7 + +... +``` + +>**Note:** +If you wish to test your app with different PHP versions and [database management systems](../../services/README.md), you can define different `image` and `services` keywords for each test job. + +#### Variables + +GitLab CI/CD allows us to use [environment variables](../../yaml/README.md#variables) in our jobs. +We defined MySQL as our database management system, which comes with a superuser root created by default. + +So we should adjust the configuration of MySQL instance by defining `MYSQL_DATABASE` variable as our database name and `MYSQL_ROOT_PASSWORD` variable as the password of `root`. +Find out more about MySQL variables at the [official MySQL Docker Image](https://hub.docker.com/r/_/mysql/). + +Also set the variables `DB_HOST` to `mysql` and `DB_USERNAME` to `root`, which are Laravel specific variables. +We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../docker/using_docker_images.md/#how-services-are-linked-to-the-build). + +```yaml +... + +variables: + MYSQL_DATABASE: homestead + MYSQL_ROOT_PASSWORD: secret + DB_HOST: mysql + DB_USERNAME: root + +... +``` + +#### Unit Test as the first job + +We defined the required shell scripts as an array of the [script](../../yaml/README.md#script) variable to be executed when running `unit_test` job. + +These scripts are some Artisan commands to prepare the Laravel, and, at the end of the script, we'll run the tests by `PHPUnit`. + +```yaml +... + +unit_test: + script: + # Install app dependencies + - composer install + # Setup .env + - cp .env.example .env + # Generate an environment key + - php artisan key:generate + # Run migrations + - php artisan migrate + # Run tests + - vendor/bin/phpunit + +... +``` + +#### Deploy to production + +The job `deploy_production` will deploy the app to the production server. +To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ssh_keys/README.md/#ssh-keys-when-using-the-docker-executor). +If the SSH keys have added successfully, we can run Envoy. + +As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well. +The [environment](../../yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment. +The `url` keyword is used to generate a link to our application on the GitLab Environments page. +The `only` keyword tells GitLab CI that the job should be executed only when the pipeline is building the `master` branch. +Lastly, `when: manual` is used to turn the job from running automatically to a manual action. + +```yaml +... + +deploy_production: + script: + # Add the private SSH key to the build environment + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + - eval $(ssh-agent -s) + - ssh-add <(echo "$SSH_PRIVATE_KEY") + - mkdir -p ~/.ssh + - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + + # Run Envoy + - ~/.composer/vendor/bin/envoy run deploy + + environment: + name: production + url: http://192.168.1.1 + when: manual + only: + - master +``` + +You may also want to add another job for [staging environment](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments), to final test your application before deploying to production. + +### Turn on GitLab CI/CD + +We have prepared everything we need to test and deploy our app with GitLab CI/CD. +To do that, commit and push `.gitlab-ci.yml` to the `master` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**. + +![pipelines page](img/pipelines_page.png) + +Here we see our **Test** and **Deploy** stages. +The **Test** stage has the `unit_test` build running. +click on it to see the Runner's output. + +![pipeline page](img/pipeline_page.png) + +After our code passed through the pipeline successfully, we can deploy to our production server by clicking the **play** button on the right side. + +![pipelines page deploy button](img/pipelines_page_deploy_button.png) + +Once the deploy pipeline passed successfully, navigate to **Pipelines > Environments**. + +![environments page](img/environments_page.png) + +If something doesn't work as expected, you can roll back to the latest working version of your app. + +![environment page](img/environment_page.png) + +By clicking on the external link icon specified on the right side, GitLab opens the production website. +Our deployment successfully was done and we can see the application is live. + +![laravel welcome page](img/laravel_welcome_page.png) + +In the case that you're interested to know how is the application directory structure on the production server after deployment, here are three directories named `current`, `releases` and `storage`. +As you know, the `current` directory is a symbolic link that points to the latest release. +The `.env` file consists of our Laravel environment variables. + +![production server app directory](img/production_server_app_directory.png) + +If you navigate to the `current` directory, you should see the application's content. +As you see, the `.env` is pointing to the `/var/www/app/.env` file and also `storage` is pointing to the `/var/www/app/storage/` directory. + +![production server current directory](img/production_server_current_directory.png) + +## Conclusion + +We configured GitLab CI to perform automated tests and used the method of [Continuous Delivery](https://continuousdelivery.com/) to deploy to production a Laravel application with Envoy, directly from the codebase. + +Envoy also was a great match to help us deploy the application without writing our custom bash script and doing Linux magics. 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/install/README.md b/doc/install/README.md index 540cb0d3f38..43197351db3 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -27,7 +27,7 @@ the hardware requirements. - [Installing in Kubernetes](kubernetes/index.md) - Install GitLab into a Kubernetes Cluster using our official Helm Chart Repository. -- [Install GitLab on OpenShift](../articles/openshift_and_gitlab/index.md) +- [Install GitLab on OpenShift](openshift_and_gitlab/index.md) - [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/) - [Install GitLab on Azure](azure/index.md) - [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md) diff --git a/doc/install/installation.md b/doc/install/installation.md index 2b7352d3561..b2acd5e78b5 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -64,7 +64,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but you can [install re2 manually](https://github.com/google/re2/wiki/Install). diff --git a/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png b/doc/install/openshift_and_gitlab/img/add-gitlab-to-project.png Binary files differindex fcad4e59ae3..fcad4e59ae3 100644 --- a/doc/articles/openshift_and_gitlab/img/add-gitlab-to-project.png +++ b/doc/install/openshift_and_gitlab/img/add-gitlab-to-project.png diff --git a/doc/articles/openshift_and_gitlab/img/add-to-project.png b/doc/install/openshift_and_gitlab/img/add-to-project.png Binary files differindex bd915a229f6..bd915a229f6 100644 --- a/doc/articles/openshift_and_gitlab/img/add-to-project.png +++ b/doc/install/openshift_and_gitlab/img/add-to-project.png diff --git a/doc/articles/openshift_and_gitlab/img/create-project-ui.png b/doc/install/openshift_and_gitlab/img/create-project-ui.png Binary files differindex e72866f252a..e72866f252a 100644 --- a/doc/articles/openshift_and_gitlab/img/create-project-ui.png +++ b/doc/install/openshift_and_gitlab/img/create-project-ui.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-logs.png b/doc/install/openshift_and_gitlab/img/gitlab-logs.png Binary files differindex 1e24080c7df..1e24080c7df 100644 --- a/doc/articles/openshift_and_gitlab/img/gitlab-logs.png +++ b/doc/install/openshift_and_gitlab/img/gitlab-logs.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-overview.png b/doc/install/openshift_and_gitlab/img/gitlab-overview.png Binary files differindex 3c5df0ea101..3c5df0ea101 100644 --- a/doc/articles/openshift_and_gitlab/img/gitlab-overview.png +++ b/doc/install/openshift_and_gitlab/img/gitlab-overview.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-running.png b/doc/install/openshift_and_gitlab/img/gitlab-running.png Binary files differindex c7db691cb30..c7db691cb30 100644 --- a/doc/articles/openshift_and_gitlab/img/gitlab-running.png +++ b/doc/install/openshift_and_gitlab/img/gitlab-running.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-scale.png b/doc/install/openshift_and_gitlab/img/gitlab-scale.png Binary files differindex 4903c7d7498..4903c7d7498 100644 --- a/doc/articles/openshift_and_gitlab/img/gitlab-scale.png +++ b/doc/install/openshift_and_gitlab/img/gitlab-scale.png diff --git a/doc/articles/openshift_and_gitlab/img/gitlab-settings.png b/doc/install/openshift_and_gitlab/img/gitlab-settings.png Binary files differindex db4360ffef0..db4360ffef0 100644 --- a/doc/articles/openshift_and_gitlab/img/gitlab-settings.png +++ b/doc/install/openshift_and_gitlab/img/gitlab-settings.png diff --git a/doc/articles/openshift_and_gitlab/img/no-resources.png b/doc/install/openshift_and_gitlab/img/no-resources.png Binary files differindex 480fb766468..480fb766468 100644 --- a/doc/articles/openshift_and_gitlab/img/no-resources.png +++ b/doc/install/openshift_and_gitlab/img/no-resources.png diff --git a/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png b/doc/install/openshift_and_gitlab/img/openshift-infra-project.png Binary files differindex 8b9f85aa341..8b9f85aa341 100644 --- a/doc/articles/openshift_and_gitlab/img/openshift-infra-project.png +++ b/doc/install/openshift_and_gitlab/img/openshift-infra-project.png diff --git a/doc/articles/openshift_and_gitlab/img/pods-overview.png b/doc/install/openshift_and_gitlab/img/pods-overview.png Binary files differindex e1cf08bd217..e1cf08bd217 100644 --- a/doc/articles/openshift_and_gitlab/img/pods-overview.png +++ b/doc/install/openshift_and_gitlab/img/pods-overview.png diff --git a/doc/articles/openshift_and_gitlab/img/rc-name.png b/doc/install/openshift_and_gitlab/img/rc-name.png Binary files differindex 889e34adbec..889e34adbec 100644 --- a/doc/articles/openshift_and_gitlab/img/rc-name.png +++ b/doc/install/openshift_and_gitlab/img/rc-name.png diff --git a/doc/articles/openshift_and_gitlab/img/running-pods.png b/doc/install/openshift_and_gitlab/img/running-pods.png Binary files differindex 3fd4e56662f..3fd4e56662f 100644 --- a/doc/articles/openshift_and_gitlab/img/running-pods.png +++ b/doc/install/openshift_and_gitlab/img/running-pods.png diff --git a/doc/articles/openshift_and_gitlab/img/storage-volumes.png b/doc/install/openshift_and_gitlab/img/storage-volumes.png Binary files differindex ae1e5381faa..ae1e5381faa 100644 --- a/doc/articles/openshift_and_gitlab/img/storage-volumes.png +++ b/doc/install/openshift_and_gitlab/img/storage-volumes.png diff --git a/doc/articles/openshift_and_gitlab/img/web-console.png b/doc/install/openshift_and_gitlab/img/web-console.png Binary files differindex aa1425d4f94..aa1425d4f94 100644 --- a/doc/articles/openshift_and_gitlab/img/web-console.png +++ b/doc/install/openshift_and_gitlab/img/web-console.png diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md new file mode 100644 index 00000000000..8fba44aea02 --- /dev/null +++ b/doc/install/openshift_and_gitlab/index.md @@ -0,0 +1,510 @@ +# Getting started with OpenShift Origin 3 and GitLab + +> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) || +> **Publication date:** 2016-06-28 + +## Introduction + +[OpenShift Origin][openshift] is an open source container application +platform created by [RedHat], based on [kubernetes] and [Docker]. That means +you can host your own PaaS for free and almost with no hassle. + +In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's +official Docker image while getting familiar with the web interface and CLI +tools that will help us achieve our goal. + +--- + +## Prerequisites + +OpenShift 3 is not yet deployed on RedHat's offered Online platform ([openshift.com]), +so in order to test it, we will use an [all-in-one Virtualbox image][vm] that is +offered by the OpenShift developers and managed by Vagrant. If you haven't done +already, go ahead and install the following components as they are essential to +test OpenShift easily: + +- [VirtualBox] +- [Vagrant] +- [OpenShift Client][oc] (`oc` for short) + +It is also important to mention that for the purposes of this tutorial, the +latest Origin release is used: + +- **oc** `v1.3.0` (must be [installed][oc-gh] locally on your computer) +- **openshift** `v1.3.0` (is pre-installed in the [VM image][vm-new]) +- **kubernetes** `v1.3.0` (is pre-installed in the [VM image][vm-new]) + +>**Note:** +If you intend to deploy GitLab on a production OpenShift cluster, there are some +limitations to bare in mind. Read on the [limitations](#current-limitations) +section for more information and follow the linked links for the relevant +discussions. + +Now that you have all batteries, let's see how easy it is to test OpenShift +on your computer. + +## Getting familiar with OpenShift Origin + +The environment we are about to use is based on CentOS 7 which comes with all +the tools needed pre-installed: Docker, kubernetes, OpenShift, etcd. + +### Test OpenShift using Vagrant + +As of this writing, the all-in-one VM is at version 1.3, and that's +what we will use in this tutorial. + +In short: + +1. Open a terminal and in a new directory run: + ```sh + vagrant init openshift/origin-all-in-one + ``` +1. This will generate a Vagrantfile based on the all-in-one VM image +1. In the same directory where you generated the Vagrantfile + enter: + + ```sh + vagrant up + ``` + +This will download the VirtualBox image and fire up the VM with some preconfigured +values as you can see in the Vagrantfile. As you may have noticed, you need +plenty of RAM (5GB in our example), so make sure you have enough. + +Now that OpenShift is setup, let's see how the web console looks like. + +### Explore the OpenShift web console + +Once Vagrant finishes its thing with the VM, you will be presented with a +message which has some important information. One of them is the IP address +of the deployed OpenShift platform and in particular <https://10.2.2.2:8443/console/>. +Open this link with your browser and accept the self-signed certificate in +order to proceed. + +Let's login as admin with username/password `admin/admin`. This is what the +landing page looks like: + +![openshift web console](img/web-console.png) + +You can see that a number of [projects] are already created for testing purposes. + +If you head over the `openshift-infra` project, a number of services with their +respective pods are there to explore. + +![openshift web console](img/openshift-infra-project.png) + +We are not going to explore the whole interface, but if you want to learn about +the key concepts of OpenShift, read the [core concepts reference][core] in the +official documentation. + +### Explore the OpenShift CLI + +OpenShift Client (`oc`), is a powerful CLI tool that talks to the OpenShift API +and performs pretty much everything you can do from the web UI and much more. + +Assuming you have [installed][oc] it, let's explore some of its main +functionalities. + +Let's first see the version of `oc`: + +```sh +$ oc version + +oc v1.3.0 +kubernetes v1.3.0+52492b4 +``` + +With `oc help` you can see the top level arguments you can run with `oc` and +interact with your cluster, kubernetes, run applications, create projects and +much more. + +Let's login to the all-in-one VM and see how to achieve the same results like +when we visited the web console earlier. The username/password for the +administrator user is `admin/admin`. There is also a test user with username/ +password `user/user`, with limited access. Let's login as admin for the moment: + +```sh +$ oc login https://10.2.2.2:8443 + +Authentication required for https://10.2.2.2:8443 (openshift) +Username: admin +Password: +Login successful. + +You have access to the following projects and can switch between them with 'oc project <projectname>': + + * cockpit + * default (current) + * delete + * openshift + * openshift-infra + * sample + +Using project "default". +``` + +Switch to the `openshift-infra` project with: + +```sh +oc project openshift-infra +``` + +And finally, see its status: + +```sh +oc status +``` + +The last command should spit a bunch of information about the statuses of the +pods and the services, which if you look closely is what we encountered in the +second image when we explored the web console. + +You can always read more about `oc` in the [OpenShift CLI documentation][oc]. + +### Troubleshooting the all-in-one VM + +Using the all-in-one VM gives you the ability to test OpenShift whenever you +want. That means you get to play with it, shutdown the VM, and pick up where +you left off. + +Sometimes though, you may encounter some issues, like OpenShift not running +when booting up the VM. The web UI may not responding or you may see issues +when trying to login with `oc`, like: + +``` +The connection to the server 10.2.2.2:8443 was refused - did you specify the right host or port? +``` + +In that case, the OpenShift service might not be running, so in order to fix it: + +1. SSH into the VM by going to the directory where the Vagrantfile is and then + run: + + ```sh + vagrant ssh + ``` + +1. Run `systemctl` and verify by the output that the `openshift` service is not + running (it will be in red color). If that's the case start the service with: + + ```sh + sudo systemctl start openshift + ``` + +1. Verify the service is up with: + + ```sh + systemctl status openshift -l + ``` + +Now you will be able to login using `oc` (like we did before) and visit the web +console. + +## Deploy GitLab + +Now that you got a taste of what OpenShift looks like, let's deploy GitLab! + +### Create a new project + +First, we will create a new project to host our application. You can do this +either by running the CLI client: + +```bash +$ oc new-project gitlab +``` + +or by using the web interface: + +![Create a new project from the UI](img/create-project-ui.png) + +If you used the command line, `oc` automatically uses the new project and you +can see its status with: + +```sh +$ oc status + +In project gitlab on server https://10.2.2.2:8443 + +You have no services, deployment configs, or build configs. +Run 'oc new-app' to create an application. +``` + +If you visit the web console, you can now see `gitlab` listed in the projects list. + +The next step is to import the OpenShift template for GitLab. + +### Import the template + +The [template][templates] is basically a JSON file which describes a set of +related object definitions to be created together, as well as a set of +parameters for those objects. + +The template for GitLab resides in the Omnibus GitLab repository under the +docker directory. Let's download it locally with `wget`: + +```bash +wget https://gitlab.com/gitlab-org/omnibus-gitlab/raw/master/docker/openshift-template.json +``` + +And then let's import it in OpenShift: + +```bash +oc create -f openshift-template.json -n openshift +``` + +>**Note:** +The `-n openshift` namespace flag is a trick to make the template available to all +projects. If you recall from when we created the `gitlab` project, `oc` switched +to it automatically, and that can be verified by the `oc status` command. If +you omit the namespace flag, the application will be available only to the +current project, in our case `gitlab`. The `openshift` namespace is a global +one that the administrators should use if they want the application to be +available to all users. + +We are now ready to finally deploy GitLab! + +### Create a new application + +The next step is to use the template we previously imported. Head over to the +`gitlab` project and hit the **Add to Project** button. + +![Add to project](img/add-to-project.png) + +This will bring you to the catalog where you can find all the pre-defined +applications ready to deploy with the click of a button. Search for `gitlab` +and you will see the previously imported template: + +![Add GitLab to project](img/add-gitlab-to-project.png) + +Select it, and in the following screen you will be presented with the predefined +values used with the GitLab template: + +![GitLab settings](img/gitlab-settings.png) + +Notice at the top that there are three resources to be created with this +template: + +- `gitlab-ce` +- `gitlab-ce-redis` +- `gitlab-ce-postgresql` + +While PostgreSQL and Redis are bundled in Omnibus GitLab, the template is using +separate images as you can see from [this line][line] in the template. + +The predefined values have been calculated for the purposes of testing out +GitLab in the all-in-one VM. You don't need to change anything here, hit +**Create** to start the deployment. + +If you are deploying to production you will want to change the **GitLab instance +hostname** and use greater values for the volume sizes. If you don't provide a +password for PostgreSQL, it will be created automatically. + +>**Note:** +The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will +resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a +trick to have distinct FQDNs pointing to services that are on our local network. +Read more on how this works in <http://xip.io>. + +Now that we configured this, let's see how to manage and scale GitLab. + +## Manage and scale GitLab + +Setting up GitLab for the first time might take a while depending on your +internet connection and the resources you have attached to the all-in-one VM. +GitLab's docker image is quite big (~500MB), so you'll have to wait until +it's downloaded and configured before you use it. + +### Watch while GitLab gets deployed + +Navigate to the `gitlab` project at **Overview**. You can notice that the +deployment is in progress by the orange color. The Docker images are being +downloaded and soon they will be up and running. + +![GitLab overview](img/gitlab-overview.png) + +Switch to the **Browse > Pods** and you will eventually see all 3 pods in a +running status. Remember the 3 resources that were to be created when we first +created the GitLab app? This is where you can see them in action. + +![Running pods](img/running-pods.png) + +You can see GitLab being reconfigured by taking look at the logs in realtime. +Click on `gitlab-ce-2-j7ioe` (your ID will be different) and go to the **Logs** +tab. + +![GitLab logs](img/gitlab-logs.png) + +At a point you should see a _**gitlab Reconfigured!**_ message in the logs. +Navigate back to the **Overview** and hopefully all pods will be up and running. + +![GitLab running](img/gitlab-running.png) + +Congratulations! You can now navigate to your new shinny GitLab instance by +visiting <http://gitlab.apps.10.2.2.2.xip.io> where you will be asked to +change the root user password. Login using `root` as username and providing the +password you just set, and start using GitLab! + +### Scale GitLab with the push of a button + +If you reach to a point where your GitLab instance could benefit from a boost +of resources, you'd be happy to know that you can scale up with the push of a +button. + +In the **Overview** page just click the up arrow button in the pod where +GitLab is. The change is instant and you can see the number of [replicas] now +running scaled to 2. + +![GitLab scale](img/gitlab-scale.png) + +Upping the GitLab pods is actually like adding new application servers to your +cluster. You can see how that would work if you didn't use GitLab with +OpenShift by following the [HA documentation][ha] for the application servers. + +Bare in mind that you may need more resources (CPU, RAM, disk space) when you +scale up. If a pod is in pending state for too long, you can navigate to +**Browse > Events** and see the reason and message of the state. + +![No resources](img/no-resources.png) + +### Scale GitLab using the `oc` CLI + +Using `oc` is super easy to scale up the replicas of a pod. You may want to +skim through the [basic CLI operations][basic-cli] to get a taste how the CLI +commands are used. Pay extra attention to the object types as we will use some +of them and their abbreviated versions below. + +In order to scale up, we need to find out the name of the replication controller. +Let's see how to do that using the following steps. + +1. Make sure you are in the `gitlab` project: + + ```sh + oc project gitlab + ``` + +1. See what services are used for this project: + + ```sh + oc get svc + ``` + + The output will be similar to: + + ``` + NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE + gitlab-ce 172.30.243.177 <none> 22/TCP,80/TCP 5d + gitlab-ce-postgresql 172.30.116.75 <none> 5432/TCP 5d + gitlab-ce-redis 172.30.105.88 <none> 6379/TCP 5d + ``` + +1. We need to see the replication controllers of the `gitlab-ce` service. + Get a detailed view of the current ones: + + ```sh + oc describe rc gitlab-ce + ``` + + This will return a large detailed list of the current replication controllers. + Search for the name of the GitLab controller, usually `gitlab-ce-1` or if + that failed at some point and you spawned another one, it will be named + `gitlab-ce-2`. + +1. Scale GitLab using the previous information: + + ```sh + oc scale --replicas=2 replicationcontrollers gitlab-ce-2 + ``` + +1. Get the new replicas number to make sure scaling worked: + + ```sh + oc get rc gitlab-ce-2 + ``` + + which will return something like: + + ``` + NAME DESIRED CURRENT AGE + gitlab-ce-2 2 2 5d + ``` + +And that's it! We successfully scaled the replicas to 2 using the CLI. + +As always, you can find the name of the controller using the web console. Just +click on the service you are interested in and you will see the details in the +right sidebar. + +![Replication controller name](img/rc-name.png) + +### Autoscaling GitLab + +In case you were wondering whether there is an option to autoscale a pod based +on the resources of your server, the answer is yes, of course there is. + +We will not expand on this matter, but feel free to read the documentation on +OpenShift's website about [autoscaling]. + +## Current limitations + +As stated in the [all-in-one VM][vm] page: + +> By default, OpenShift will not allow a container to run as root or even a +non-random container assigned userid. Most Docker images in the Dockerhub do not +follow this best practice and instead run as root. + +The all-in-one VM we are using has this security turned off so it will not +bother us. In any case, it is something to keep in mind when deploying GitLab +on a production cluster. + +In order to deploy GitLab on a production cluster, you will need to assign the +GitLab service account to the `anyuid` Security Context. + +1. Edit the Security Context: + ```sh + oc edit scc anyuid + ``` + +1. Add `system:serviceaccount:<project>:gitlab-ce-user` to the `users` section. + If you changed the Application Name from the default the user will + will be `<app-name>-user` instead of `gitlab-ce-user` + +1. Save and exit the editor + +## Conclusion + +By now, you should have an understanding of the basic OpenShift Origin concepts +and a sense of how things work using the web console or the CLI. + +GitLab was hard to install in previous versions of OpenShift, +but now that belongs to the past. Upload a template, create a project, add an +application and you are done. You are ready to login to your new GitLab instance. + +And remember that in this tutorial we just scratched the surface of what Origin +is capable of. As always, you can refer to the detailed +[documentation][openshift-docs] to learn more about deploying your own OpenShift +PaaS and managing your applications with the ease of containers. + +[RedHat]: https://www.redhat.com/en "RedHat website" +[openshift]: https://www.openshift.org "OpenShift Origin website" +[vm]: https://www.openshift.org/vm/ "OpenShift All-in-one VM" +[vm-new]: https://atlas.hashicorp.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Atlas" +[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab" +[openshift.com]: https://openshift.com "OpenShift Online" +[kubernetes]: http://kubernetes.io/ "Kubernetes website" +[Docker]: https://www.docker.com "Docker website" +[oc]: https://docs.openshift.org/latest/cli_reference/get_started_cli.html "Documentation - oc CLI documentation" +[VirtualBox]: https://www.virtualbox.org/wiki/Downloads "VirtualBox downloads" +[Vagrant]: https://www.vagrantup.com/downloads.html "Vagrant downloads" +[projects]: https://docs.openshift.org/latest/dev_guide/projects.html "Documentation - Projects overview" +[core]: https://docs.openshift.org/latest/architecture/core_concepts/index.html "Documentation - Core concepts of OpenShift Origin" +[templates]: https://docs.openshift.org/latest/architecture/core_concepts/templates.html "Documentation - OpenShift templates" +[old-post]: https://blog.openshift.com/deploy-gitlab-openshift/ "Old post - Deploy GitLab on OpenShift" +[line]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239 "GitLab - OpenShift template" +[oc-gh]: https://github.com/openshift/origin/releases/tag/v1.3.0 "Openshift 1.3.0 release on GitHub" +[ha]: ../../administration/high_availability/gitlab.html "Documentation - GitLab High Availability" +[replicas]: https://docs.openshift.org/latest/architecture/core_concepts/deployments.html#replication-controllers "Documentation - Replication controller" +[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale" +[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations" +[openshift-docs]: https://docs.openshift.org "OpenShift documentation" diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 54c3e20d61d..50bb665216e 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -5,8 +5,8 @@ An application data backup creates an archive file that contains the database, all repositories and all attachments. -You can only restore a backup to **exactly the same version and type (CE/EE)** -of GitLab on which it was created. The best way to migrate your repositories +You can only restore a backup to **exactly the same version and type (CE/EE)** +of GitLab on which it was created. The best way to migrate your repositories from one server to another is through backup restore. ## Backup @@ -14,6 +14,19 @@ from one server to another is through backup restore. GitLab provides a simple command line interface to backup your whole installation, and is flexible enough to fit your needs. +### Requirements + +If you're using GitLab with the Omnibus package, you're all set. If you +installed GitLab from source, make sure the following packages are installed: + +* rsync + +If you're using Ubuntu, you could run: + +``` +sudo apt-get install -y rsync +``` + ### Backup timestamp >**Note:** @@ -431,7 +444,7 @@ The [restore prerequisites section](#restore-prerequisites) includes crucial information. Make sure to read and test the whole restore process at least once before attempting to perform it in a production environment. -You can only restore a backup to **exactly the same version and type (CE/EE)** of +You can only restore a backup to **exactly the same version and type (CE/EE)** of GitLab that you created it on, for example CE 9.1.0. ### Restore prerequisites @@ -511,7 +524,7 @@ sudo service gitlab restart This procedure assumes that: -- You have installed the **exact same version and type (CE/EE)** of GitLab +- You have installed the **exact same version and type (CE/EE)** of GitLab Omnibus with which the backup was created. - You have run `sudo gitlab-ctl reconfigure` at least once. - GitLab is running. If not, start it using `sudo gitlab-ctl start`. diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md new file mode 100644 index 00000000000..cdf61057449 --- /dev/null +++ b/doc/topics/git/how_to_install_git/index.md @@ -0,0 +1,66 @@ +# Installing Git + +> **[Article Type](../../../development/writing_documentation.html#types-of-technical-articles):** user guide || +> **Level:** beginner || +> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) || +> **Publication date:** 2017-05-15 + +To begin contributing to GitLab projects +you will need to install the Git client on your computer. +This article will show you how to install Git on macOS, Ubuntu Linux and Windows. + +## Install Git on macOS using the Homebrew package manager + +Although it is easy to use the version of Git shipped with macOS +or install the latest version of Git on macOS by downloading it from the project website, +we recommend installing it via Homebrew to get access to +an extensive selection of dependancy managed libraries and applications. + +If you are sure you don't need access to any additional development libraries +or don't have approximately 15gb of available disk space for Xcode and Homebrew +use one of the the aforementioned methods. + +### Installing Xcode + +Xcode is needed by Homebrew to build dependencies. +You can install [XCode](https://developer.apple.com/xcode/) +through the macOS App Store. + +### Installing Homebrew + +Once Xcode is installed browse to the [Homebrew website](http://brew.sh/index.html) +for the official Homebrew installation instructions. + +### Installing Git via Homebrew + +With Homebrew installed you are now ready to install Git. +Open a Terminal and enter in the following command: + +```bash +brew install git +``` + +Congratulations you should now have Git installed via Homebrew. +Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md). + +## Install Git on Ubuntu Linux + +On Ubuntu and other Linux operating systems +it is recommended to use the built in package manager to install Git. + +Open a Terminal and enter in the following commands +to install the latest Git from the official Git maintained package archives: + +```bash +sudo apt-add-repository ppa:git-core/ppa +sudo apt-get update +sudo apt-get install git +``` + +Congratulations you should now have Git installed via the Ubuntu package manager. +Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md). + +## Installing Git on Windows from the Git website + +Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows. +Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md). diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index 588f4fa369f..f69e2e49f0c 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -14,6 +14,7 @@ We've gathered some resources to help you to get the best from Git with GitLab. ## Getting started - [Git concepts](../../university/training/user_training.md#git-concepts) +- [How to install Git](how_to_install_git/index.md) - [Start using Git on the command line](../../gitlab-basics/start-using-git.md) - [Command Line basic commands](../../gitlab-basics/command-line-commands.md) - [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) @@ -21,27 +22,39 @@ We've gathered some resources to help you to get the best from Git with GitLab. - [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit) - [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit) - [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase) -- **Articles:** - - [Numerous _undo_ possibilities in Git](../../articles/numerous_undo_possibilities_in_git/index.md) - - [How to install Git](../../articles/how_to_install_git/index.md) - - [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) - - [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/) -- **Presentations:** - - [GLU Course: About Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing) -- **Third-party resources:** - - What is [Git](https://git-scm.com) - - [Version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control) - - [Getting Started - Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics) - - [Getting Started - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - - [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab) + +**Third-party references:** + +- [Getting Started - Git website](https://git-scm.com) +- [Getting Started - Version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control) +- [Getting Started - Git Basics](https://git-scm.com/book/en/v2/Getting-Started-Git-Basics) +- [Getting Started - Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab) + +### Concepts + +- Article (2017-05-17): [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/) +- Article (2016-05-11): [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/) +- GLU Course (Presentation): [About Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing) + +## Exploring Git + +- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) +- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/) + +## Troubleshooting Git + +- [Numerous _undo_ possibilities in Git](../../articles/numerous_undo_possibilities_in_git/index.md) +- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out. ## Branching strategies -- **Articles:** - - [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/) -- **Third-party resources:** - - [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell) - - [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows) +- [GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/) + +**Third-party references:** + +- [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell) +- [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows) ## Advanced use @@ -55,17 +68,7 @@ We've gathered some resources to help you to get the best from Git with GitLab. ## Git LFS -- [Git LFS](../../workflow/lfs/manage_large_binaries_with_git_lfs.md) +- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) +- [GitLab Git LFS documentation](../../workflow/lfs/manage_large_binaries_with_git_lfs.md) - [Git-Annex to Git-LFS migration guide](https://docs.gitlab.com/ee/workflow/lfs/migrate_from_git_annex_to_git_lfs.html) -- **Articles:** - - [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) - - [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/) - -## Troubleshooting - -- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out. - -## General information - -- **Articles:** - - [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/2016/05/11/git-repository-pricing/) +- Article (2015-08-13): [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/) diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index 54625996dff..ddc853afded 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -26,6 +26,10 @@ Login to your AWS account through the `My Account` dropdown on Amazon Web Services console from where we can choose all of the services we'll be using to configure our cloud infrastructure. +### Reference Architecture + +![Reference Architecture](img/reference-arch.png) + *** ## Network diff --git a/doc/university/high-availability/aws/img/reference-arch.png b/doc/university/high-availability/aws/img/reference-arch.png Binary files differnew file mode 100644 index 00000000000..271ee5bc614 --- /dev/null +++ b/doc/university/high-availability/aws/img/reference-arch.png diff --git a/doc/update/10.3-to-10.4.md b/doc/update/10.3-to-10.4.md index 850cb3103f4..67b7e634c94 100644 --- a/doc/update/10.3-to-10.4.md +++ b/doc/update/10.3-to-10.4.md @@ -21,6 +21,8 @@ sudo service gitlab stop ### 2. Backup +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + ```bash cd /home/git/gitlab 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/features/support/env.rb b/features/support/env.rb index 91a92314959..7f5b4c1c11b 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -10,14 +10,14 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper).each do |f| +WebMock.enable! + +%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f| require Rails.root.join('spec', 'support', f) end Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } -WebMock.allow_net_connect! - Spinach.hooks.before_run do include RSpec::Mocks::ExampleMethods RSpec::Mocks.setup 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 283134e043e..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 @@ -1103,14 +1118,27 @@ module Gitlab end end - def write_ref(ref_path, ref) + def write_ref(ref_path, ref, old_ref: nil, shell: true) + if shell + shell_write_ref(ref_path, ref, old_ref) + else + rugged_write_ref(ref_path, ref) + end + end + + def shell_write_ref(ref_path, ref, old_ref) raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") + raise ArgumentError, "invalid old_ref #{old_ref.inspect}" if !old_ref.nil? && old_ref.include?("\x00") - input = "update #{ref_path}\x00#{ref}\x00\x00" + input = "update #{ref_path}\x00#{ref}\x00#{old_ref}\x00" run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) } end + def rugged_write_ref(ref_path, ref) + rugged.references.create(ref_path, ref, force: true) + end + def fetch_ref(source_repository, source_ref:, target_ref:) Gitlab::Git.check_namespace!(source_repository) source_repository = RemoteRepository.new(source_repository) unless source_repository.is_a?(RemoteRepository) @@ -1475,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 @@ -1496,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/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index d8f1a919522..f82ed6300cc 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -750,7 +750,7 @@ describe 'Copy as GFM', :js do js = <<-JS.strip_heredoc (function(selector) { var els = document.querySelectorAll(selector); - var htmls = _.map(els, function(el) { return el.outerHTML; }); + var htmls = [].slice.call(els).map(function(el) { return el.outerHTML; }); return htmls.join("\\n"); })("#{escape_javascript(selector)}") JS 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/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb index 41f3c15a94c..b650c1f4197 100644 --- a/spec/features/projects/commits/user_browses_commits_spec.rb +++ b/spec/features/projects/commits/user_browses_commits_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'User broweses commits' do +describe 'User browses commits' do let(:user) { create(:user) } let(:project) { create(:project, :repository, namespace: user.namespace) } @@ -31,6 +31,19 @@ describe 'User broweses commits' do check_author_link(RepoHelpers.sample_commit.author_email, user) end end + + context 'when the blob does not exist' do + let(:commit) { create(:commit, project: project) } + + it 'shows a blank label' do + allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + allow_any_instance_of(Gitlab::Diff::File).to receive(:raw_binary?).and_return(true) + + visit(project_commit_path(project, commit)) + + expect(find('.diff-file-changes', visible: false)).to have_content('No file name available') + end + end end private 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/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 645ce831b53..e5e7b48228b 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; - +import _ from 'underscore'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 9ae2d535398..0671facb285 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,5 +1,6 @@ /* global BoardService */ /* eslint-disable comma-dangle, no-unused-vars, quote-props */ +import _ from 'underscore'; export const listObj = { id: 300, diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index 9fc047b1f5e..d62c2966a8b 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 0ca9290d3d2..b870f87eab9 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import eventHub from '~/deploy_keys/eventhub'; import deployKeysApp from '~/deploy_keys/components/app.vue'; diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index d02adb25b4e..a41a4e5a3f7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsComponent from '~/environments/components/environments_app.vue'; import { environment, folder } from './mock_data'; diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 4ea4d9d7499..a085074d312 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import { environmentsList } from '../mock_data'; diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 2ecb64d84b5..0684c3498a2 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; 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/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js index 2b7ee65094b..30441faf844 100644 --- a/spec/javascripts/issue_show/components/fields/description_template_spec.js +++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import descriptionTemplate from '~/issue_show/components/fields/description_template.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Issue description template component', () => { let vm; diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 000b53af016..50ce019c32a 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -1,7 +1,5 @@ import Vue from 'vue'; import formComponent from '~/issue_show/components/form.vue'; -import '~/templates/issuable_template_selector'; -import '~/templates/issuable_template_selectors'; describe('Inline edit form component', () => { let vm; diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e983e4de3fc..5d0ee91d977 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 7c8d6685ee1..36c56cd3862 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index c8a6cb7e612..cb63b64724d 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,4 +1,4 @@ - +import _ from 'underscore'; import Vue from 'vue'; import store from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; 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/notes_spec.js b/spec/javascripts/notes_spec.js index 167f074fb9b..a40821a5693 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,5 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ +import _ from 'underscore'; import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; 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/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index 9fec2f61f78..bc6413a159f 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 367b42cefb0..a99ebc4e51a 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 1b96b2e3d51..61c2f783acc 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import stage from '~/pipelines/components/stage.vue'; diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 87259fe0bab..6a8a85e3dfb 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; 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/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index b97e24d9dcf..6bb6d639f24 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 9efd109b996..afa18cc127e 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 8b0d51bbcc8..97f762d07a7 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; 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/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 1c87fcec245..7265e1b6cb5 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import SmartInterval from '~/smart_interval'; describe('SmartInterval', function () { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 6897c991066..2f6691df9cd 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,6 +1,5 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; @@ -31,7 +30,6 @@ jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; // globalize common libraries window.$ = window.jQuery = $; -window._ = _; // stub expected globals window.gl = window.gl || {}; 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/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index 8450ad9dbcb..adf80d0c2bb 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 45a0bb0650f..8edba1f47a3 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,4 @@ -/* global Mousetrap */ +import Mousetrap from 'mousetrap'; import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; 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/models/project_spec.rb b/spec/models/project_spec.rb index 00afa09f1a3..78223c44999 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1871,9 +1871,8 @@ describe Project do end it 'creates the new reference with rugged' do - expect(project.repository.rugged.references).to receive(:create).with('HEAD', - "refs/heads/#{project.default_branch}", - force: true) + expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", shell: false) + project.change_head(project.default_branch) end 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 |