diff options
author | Clement Ho <clemmakesapps@gmail.com> | 2018-02-27 20:08:20 +0000 |
---|---|---|
committer | Clement Ho <clemmakesapps@gmail.com> | 2018-02-27 20:08:20 +0000 |
commit | f20ee984d2322eef9908134d041e446e259b585c (patch) | |
tree | d0eb70b62d9f702cd7132a279d5b56bd36400a69 | |
parent | 124ce3b6e31f2da678629eb9bf68c2f92aa1bd55 (diff) | |
parent | d4867c518f2d72125c619c191fa9577dabb1ae9a (diff) | |
download | gitlab-ce-f20ee984d2322eef9908134d041e446e259b585c.tar.gz |
Merge branch 'master' into 'pipelines_show_refactor'
# Conflicts:
# config/webpack.config.js
320 files changed, 2549 insertions, 1713 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index b02fe54a4ff..333a70f71d3 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,8 +11,8 @@ engines: exclude_paths: - "lib/api/v3/*" eslint: - # eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4 - enabled: false + enabled: true + channel: "eslint-4" rubocop: enabled: true channel: "gitlab-rubocop-0-52-1" diff --git a/CHANGELOG.md b/CHANGELOG.md index 869884f8ca6..c8d399b2b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.5.2 (2018-02-25) + +### Fixed (7 changes) + +- Fix single digit value clipping for stacked progress bar. !17217 +- Fix issue with cache key being empty when variable used as the key. !17260 +- Enable Legacy Authorization by default on Cluster creations. !17302 +- Allow branch names to be named the same as the sha it points to. +- Fix 500 error when loading an invalid upload URL. +- Don't attempt to update user tracked fields if database is in read-only. +- Prevent MR Widget error when no CI configured. + +### Performance (5 changes) + +- Improve query performance for snippets dashboard. !17088 +- Only check LFS integrity for first ref in a push to avoid timeout. !17098 +- Improve query performance of MembersFinder. !17190 +- Increase feature flag cache TTL to one hour. +- Improve performance of searching for and autocompleting of users. + + ## 10.5.1 (2018-02-22) - No changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfe4bf65f9f..b70d2da5bea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -397,9 +397,9 @@ For issues related to the open source stewardship of GitLab, there is the ~"stewardship" label. This label is to be used for issues in which the stewardship of GitLab -is a topic of discussion. For instance if GitLab Inc. is planning to remove -features from GitLab CE to make exclusive in GitLab EE, related issues -would be labelled with ~"stewardship". +is a topic of discussion. For instance if GitLab Inc. is planning to add +features from GitLab EE to GitLab CE, related issues would be labelled with +~"stewardship". A recent example of this was the issue for [bringing the time tracking API to GitLab CE][time-tracking-issue]. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 92fc430ae8f..137c1281121 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.82.0 +0.85.0 @@ -411,7 +411,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.85.0', require: 'gitaly' # Locked until https://github.com/google/protobuf/issues/4210 is closed gem 'google-protobuf', '= 3.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 57ff086f0b1..89b86ae0259 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -285,7 +285,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.84.0) + gitaly-proto (0.85.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -1057,7 +1057,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.84.0) + gitaly-proto (~> 0.85.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 9a0442e2afe..6637904d87d 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,6 +1,6 @@ <script> import Sortable from 'vendor/Sortable'; -import boardNewIssue from './board_new_issue'; +import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.vue index bc28f7f45f4..efface7143d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ -/* global ListIssue */ +<script> import eventHub from '../eventhub'; +import ListIssue from '../models/issue'; const Store = gl.issueBoards.BoardsStore; @@ -17,6 +18,9 @@ export default { error: false, }; }, + mounted() { + this.$refs.input.focus(); + }, methods: { submit(e) { e.preventDefault(); @@ -59,42 +63,51 @@ export default { eventHub.$emit(`hide-issue-form-${this.list.id}`); }, }, - mounted() { - this.$refs.input.focus(); - }, - template: ` - <div class="card board-new-issue-form"> - <form @submit="submit($event)"> - <div class="flash-container" - v-if="error"> - <div class="flash-alert"> - An error occurred. Please try again. - </div> - </div> - <label class="label-light" - :for="list.id + '-title'"> - Title - </label> - <input class="form-control" - type="text" - v-model="title" - ref="input" - autocomplete="off" - :id="list.id + '-title'" /> - <div class="clearfix prepend-top-10"> - <button class="btn btn-success pull-left" - type="submit" - :disabled="title === ''" - ref="submit-button"> - Submit issue - </button> - <button class="btn btn-default pull-right" - type="button" - @click="cancel"> - Cancel - </button> - </div> - </form> - </div> - `, }; +</script> + +<template> + <div class="card board-new-issue-form"> + <form @submit="submit($event)"> + <div + class="flash-container" + v-if="error" + > + <div class="flash-alert"> + An error occurred. Please try again. + </div> + </div> + <label + class="label-light" + :for="list.id + '-title'" + > + Title + </label> + <input + class="form-control" + type="text" + v-model="title" + ref="input" + autocomplete="off" + :id="list.id + '-title'" + /> + <div class="clearfix prepend-top-10"> + <button + class="btn btn-success pull-left" + type="submit" + :disabled="title === ''" + ref="submit-button" + > + Submit issue + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="cancel" + > + Cancel + </button> + </div> + </form> + </div> +</template> diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/index.js index 90166b3d3d1..8e31f1865f0 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/index.js @@ -2,11 +2,13 @@ import _ from 'underscore'; import Vue from 'vue'; -import Flash from '../flash'; -import { __ } from '../locale'; + +import Flash from '~/flash'; +import { __ } from '~/locale'; + import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; -import sidebarEventHub from '../sidebar/event_hub'; +import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first import './models/issue'; import './models/label'; import './models/list'; @@ -22,9 +24,9 @@ import './components/board'; import './components/board_sidebar'; import './components/new_list_dropdown'; import './components/modal/index'; -import '../vue_shared/vue_resource_interceptor'; +import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first -$(() => { +export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; const ModalStore = gl.issueBoards.ModalStore; @@ -236,4 +238,4 @@ $(() => { </div> `, }); -}); +}; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 81edd95bf2b..3bfb6d39ad5 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -110,3 +110,5 @@ class ListIssue { } window.ListIssue = ListIssue; + +export default ListIssue; diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 798d7e0d147..348cdeec737 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -2,7 +2,7 @@ /* global List */ import _ from 'underscore'; import Cookies from 'js-cookie'; -import { getUrlParamsArray } from '../../lib/utils/common_utils'; +import { getUrlParamsArray } from '~/lib/utils/common_utils'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 1f9153d95bd..3d89bf1316e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable); window.gl = window.gl || {}; window.gl.CommitPipelinesTable = CommitPipelinesTable; -document.addEventListener('DOMContentLoaded', () => { +export default () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl) { @@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => { pipelineTableViewEl.appendChild(table.$el); } } -}); +}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index acf0effa00d..1ccf96a75dc 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -6,177 +6,80 @@ import GlFieldErrors from './gl_field_errors'; import Shortcuts from './shortcuts'; import SearchAutocomplete from './search_autocomplete'; -var Dispatcher; - -(function() { - Dispatcher = (function() { - function Dispatcher() { - this.initSearch(); - this.initFieldErrors(); - this.initPageScripts(); - } - - Dispatcher.prototype.initPageScripts = function() { - var path, shortcut_handler; - const page = $('body').attr('data-page'); - if (!page) { - return false; - } - - const fail = () => Flash('Error loading dynamic module'); - const callDefault = m => m.default(); - - path = page.split(':'); - shortcut_handler = null; - - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); - gfm.setup($(el), { - emojis: true, - members: enableGFM, - issues: enableGFM, - milestones: enableGFM, - mergeRequests: enableGFM, - labels: enableGFM, - }); - }); - - const shortcutHandlerPages = [ - 'projects:activity', - 'projects:artifacts:browse', - 'projects:artifacts:file', - 'projects:blame:show', - 'projects:blob:show', - 'projects:commit:show', - 'projects:commits:show', - 'projects:find_file:show', - 'projects:issues:edit', - 'projects:issues:index', - 'projects:issues:new', - 'projects:issues:show', - 'projects:merge_requests:creations:diffs', - 'projects:merge_requests:creations:new', - 'projects:merge_requests:edit', - 'projects:merge_requests:index', - 'projects:merge_requests:show', - 'projects:network:show', - 'projects:show', - 'projects:tree:show', - 'groups:show', - ]; +function initSearch() { + // Only when search form is present + if ($('.search').length) { + return new SearchAutocomplete(); + } +} - if (shortcutHandlerPages.indexOf(page) !== -1) { - shortcut_handler = true; - } +function initFieldErrors() { + $('.gl-show-field-errors').each((i, form) => { + new GlFieldErrors(form); + }); +} - switch (path[0]) { - case 'admin': - switch (path[1]) { - case 'broadcast_messages': - import('./pages/admin/broadcast_messages') - .then(callDefault) - .catch(fail); - break; - case 'cohorts': - import('./pages/admin/cohorts') - .then(callDefault) - .catch(fail); - break; - case 'groups': - switch (path[2]) { - case 'show': - import('./pages/admin/groups/show') - .then(callDefault) - .catch(fail); - break; - } - break; - case 'projects': - 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': - import('./pages/admin/labels/edit') - .then(callDefault) - .catch(fail); - break; - } - case 'abuse_reports': - import('./pages/admin/abuse_reports') - .then(callDefault) - .catch(fail); - break; - } - break; - case 'profiles': - import('./pages/profiles/index') - .then(callDefault) - .catch(fail); - break; - case 'projects': - import('./pages/projects') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - switch (path[1]) { - case 'compare': - import('./pages/projects/compare') - .then(callDefault) - .catch(fail); - break; - case 'create': - case 'new': - import('./pages/projects/new') - .then(callDefault) - .catch(fail); - break; - case 'wikis': - import('./pages/projects/wikis') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; - } - break; - } - // If we haven't installed a custom shortcut handler, install the default one - if (!shortcut_handler) { - new Shortcuts(); - } +function initPageShortcuts(page) { + const pagesWithCustomShortcuts = [ + 'projects:activity', + 'projects:artifacts:browse', + 'projects:artifacts:file', + 'projects:blame:show', + 'projects:blob:show', + 'projects:commit:show', + 'projects:commits:show', + 'projects:find_file:show', + 'projects:issues:edit', + 'projects:issues:index', + 'projects:issues:new', + 'projects:issues:show', + 'projects:merge_requests:creations:diffs', + 'projects:merge_requests:creations:new', + 'projects:merge_requests:edit', + 'projects:merge_requests:index', + 'projects:merge_requests:show', + 'projects:network:show', + 'projects:show', + 'projects:tree:show', + 'groups:show', + ]; - if (document.querySelector('#peek')) { - import('./performance_bar') - .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap - .catch(fail); - } - }; + if (pagesWithCustomShortcuts.indexOf(page) === -1) { + new Shortcuts(); + } +} - Dispatcher.prototype.initSearch = function() { - // Only when search form is present - if ($('.search').length) { - return new SearchAutocomplete(); - } - }; +function initGFMInput() { + $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { + const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + gfm.setup($(el), { + emojis: true, + members: enableGFM, + issues: enableGFM, + milestones: enableGFM, + mergeRequests: enableGFM, + labels: enableGFM, + }); + }); +} - Dispatcher.prototype.initFieldErrors = function() { - $('.gl-show-field-errors').each((i, form) => { - new GlFieldErrors(form); - }); - }; +function initPerformanceBar() { + if (document.querySelector('#peek')) { + import('./performance_bar') + .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap + .catch(() => Flash('Error loading performance bar module')); + } +} - return Dispatcher; - })(); -})(); +export default () => { + initSearch(); + initFieldErrors(); -export default function initDispatcher() { - return new Dispatcher(); -} + const page = $('body').attr('data-page'); + if (page) { + initPageShortcuts(page); + initGFMInput(); + initPerformanceBar(); + } +}; diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index b4eca47957e..22863e926d4 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -2,8 +2,8 @@ /** * Render environments table. */ +import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import environmentItem from './environment_item.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 5d2d14c7682..de0fbdb2e91 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#environments-folder-list-view', components: { environmentsFolderApp, @@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js deleted file mode 100644 index b693084e434..00000000000 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ /dev/null @@ -1,102 +0,0 @@ -import eventHub from '../event_hub'; -import FilteredSearchTokenizer from '../filtered_search_tokenizer'; - -export default { - name: 'RecentSearchesDropdownContent', - - props: { - items: { - type: Array, - required: true, - }, - isLocalStorageAvailable: { - type: Boolean, - required: false, - default: true, - }, - allowedKeys: { - type: Array, - required: true, - }, - }, - - computed: { - processedItems() { - return this.items.map((item) => { - const { tokens, searchToken } - = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); - - const resultantTokens = tokens.map(token => ({ - prefix: `${token.key}:`, - suffix: `${token.symbol}${token.value}`, - })); - - return { - text: item, - tokens: resultantTokens, - searchToken, - }; - }); - }, - hasItems() { - return this.items.length > 0; - }, - }, - - methods: { - onItemActivated(text) { - eventHub.$emit('recentSearchesItemSelected', text); - }, - onRequestClearRecentSearches(e) { - // Stop the dropdown from closing - e.stopPropagation(); - - eventHub.$emit('requestClearRecentSearches'); - }, - }, - - template: ` - <div> - <div - v-if="!isLocalStorageAvailable" - class="dropdown-info-note"> - This feature requires local storage to be enabled - </div> - <ul v-else-if="hasItems"> - <li - v-for="(item, index) in processedItems" - :key="index"> - <button - type="button" - class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text)"> - <span> - <span - v-for="(token, tokenIndex) in item.tokens" - class="filtered-search-history-dropdown-token"> - <span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> - </span> - </span> - <span class="filtered-search-history-dropdown-search-token"> - {{ item.searchToken }} - </span> - </button> - </li> - <li class="divider"></li> - <li> - <button - type="button" - class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event)"> - Clear recent searches - </button> - </li> - </ul> - <div - v-else - class="dropdown-info-note"> - You don't have any recent searches - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue new file mode 100644 index 00000000000..26618af9515 --- /dev/null +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -0,0 +1,104 @@ +<script> +import eventHub from '../event_hub'; +import FilteredSearchTokenizer from '../filtered_search_tokenizer'; + +export default { + name: 'RecentSearchesDropdownContent', + props: { + items: { + type: Array, + required: true, + }, + isLocalStorageAvailable: { + type: Boolean, + required: false, + default: true, + }, + allowedKeys: { + type: Array, + required: true, + }, + }, + computed: { + processedItems() { + return this.items.map((item) => { + const { tokens, searchToken } + = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); + + const resultantTokens = tokens.map(token => ({ + prefix: `${token.key}:`, + suffix: `${token.symbol}${token.value}`, + })); + + return { + text: item, + tokens: resultantTokens, + searchToken, + }; + }); + }, + hasItems() { + return this.items.length > 0; + }, + }, + methods: { + onItemActivated(text) { + eventHub.$emit('recentSearchesItemSelected', text); + }, + onRequestClearRecentSearches(e) { + // Stop the dropdown from closing + e.stopPropagation(); + + eventHub.$emit('requestClearRecentSearches'); + }, + }, +}; +</script> +<template> + <div> + <div + v-if="!isLocalStorageAvailable" + class="dropdown-info-note"> + This feature requires local storage to be enabled + </div> + <ul v-else-if="hasItems"> + <li + v-for="(item, index) in processedItems" + :key="`processed-items-${index}`" + > + <button + type="button" + class="filtered-search-history-dropdown-item" + @click="onItemActivated(item.text)"> + <span> + <span + class="filtered-search-history-dropdown-token" + v-for="(token, index) in item.tokens" + :key="`dropdown-token-${index}`" + > + <span class="name">{{ token.prefix }}</span> + <span class="value">{{ token.suffix }}</span> + </span> + </span> + <span class="filtered-search-history-dropdown-search-token"> + {{ item.searchToken }} + </span> + </button> + </li> + <li class="divider"></li> + <li> + <button + type="button" + class="filtered-search-history-clear-button" + @click="onRequestClearRecentSearches($event)"> + Clear recent searches + </button> + </li> + </ul> + <div + v-else + class="dropdown-info-note"> + You don't have any recent searches + </div> + </div> +</template> diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index c99ed63c4af..f9338b82acf 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; +import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue'; import eventHub from './event_hub'; class RecentSearchesRoot { @@ -33,7 +33,7 @@ class RecentSearchesRoot { this.vm = new Vue({ el: this.wrapperElement, components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + RecentSearchesDropdownContent, }, data() { return state; }, template: ` diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 110918872fb..cbbab765e1c 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -1,9 +1,9 @@ <script> import { mapState } from 'vuex'; - import timeAgoMixin from '../../vue_shared/mixins/timeago'; - import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; + import timeAgoMixin from '~/vue_shared/mixins/timeago'; + import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; + import fileIcon from '~/vue_shared/components/file_icon.vue'; import newDropdown from './new_dropdown/index.vue'; - import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 5ed7bddf6ae..5656081c598 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; - import fileIcon from '../../vue_shared/components/file_icon.vue'; + import fileIcon from '~/vue_shared/components/file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 5de48aa49a9..89a246f56cf 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -213,7 +213,7 @@ export default class LabelsSelect { } } if (label.duplicate) { - color = gl.DropdownUtils.duplicateLabelColor(label.color); + color = DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { @@ -242,10 +242,16 @@ export default class LabelsSelect { filterable: true, selected: $dropdown.data('selected') || [], toggleLabel: function(selected, el) { + var $dropdownParent = $dropdown.parent(); + var $dropdownInputField = $dropdownParent.find('.dropdown-input-field'); var isSelected = el !== null ? el.hasClass('is-active') : false; var title = selected.title; var selectedLabels = this.selected; + if ($dropdownInputField.length && $dropdownInputField.val().length) { + $dropdownParent.find('.dropdown-input-clear').trigger('click'); + } + if (selected.id === 0) { this.selected = []; return 'No Label'; diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js deleted file mode 100644 index 129f1724cb8..00000000000 --- a/app/assets/javascripts/network/network_bundle.js +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ - -import ShortcutsNetwork from '../shortcuts_network'; -import Network from './network'; - -$(function() { - if (!$(".network-graph").length) return; - - var network_graph; - network_graph = new Network({ - url: $(".network-graph").attr('data-url'), - commit_url: $(".network-graph").attr('data-commit-url'), - ref: $(".network-graph").attr('data-ref'), - commit_id: $(".network-graph").attr('data-commit-id') - }); - return new ShortcutsNetwork(network_graph.branch_graph); -}); diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js index c0b6e8d4095..d76b1f174fc 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/index.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js @@ -1,3 +1,3 @@ import AbuseReports from './abuse_reports'; -export default () => new AbuseReports(); +document.addEventListener('DOMContentLoaded', () => new AbuseReports()); diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index b68ce5d32d8..f92450cbaa7 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; -export default function initBroadcastMessagesForm() { +export default () => { $('input#broadcast_message_color').on('input', function onMessageColorInput() { const previewColor = $(this).val(); $('div.broadcast-message-preview').css('background-color', previewColor); @@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() { .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); } }, 250)); -} +}; diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js index b548c48282a..d6cc6a850eb 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js @@ -1,3 +1,3 @@ import initBroadcastMessagesForm from './broadcast_message'; -export default () => initBroadcastMessagesForm(); +document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js index 42ef9d38ef7..2d5020dbef4 100644 --- a/app/assets/javascripts/pages/admin/cohorts/index.js +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -1,3 +1,3 @@ import initUsagePing from './usage_ping'; -export default () => initUsagePing(); +document.addEventListener('DOMContentLoaded', initUsagePing); diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js index 5defea104d4..b0cdad627a6 100644 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -1,3 +1,3 @@ import UsersSelect from '../../../../users_select'; -export default () => new UsersSelect(); +document.addEventListener('DOMContentLoaded', () => new UsersSelect()); diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js index d7ec6e47f67..5de1d4d6344 100644 --- a/app/assets/javascripts/pages/admin/labels/edit/index.js +++ b/app/assets/javascripts/pages/admin/labels/edit/index.js @@ -1,3 +1,3 @@ import Labels from '../../../../labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js index d7ec6e47f67..5de1d4d6344 100644 --- a/app/assets/javascripts/pages/admin/labels/new/index.js +++ b/app/assets/javascripts/pages/admin/labels/new/index.js @@ -1,3 +1,3 @@ import Labels from '../../../../labels'; -export default () => new Labels(); +document.addEventListener('DOMContentLoaded', () => new Labels()); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 71e0ddcd7b6..31c96eb87af 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,9 +1,9 @@ import ProjectsList from '../../../projects_list'; import NamespaceSelect from '../../../namespace_select'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new document.querySelectorAll('.js-namespace-select') .forEach(dropdown => new NamespaceSelect({ dropdown })); -}; +}); diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js index 90eed38777a..9bd430f4f11 100644 --- a/app/assets/javascripts/pages/profiles/index/index.js +++ b/app/assets/javascripts/pages/profiles/index/index.js @@ -1,7 +1,7 @@ import NotificationsForm from '../../../notifications_form'; import notificationsDropdown from '../../../notifications_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new NotificationsForm(); // eslint-disable-line no-new notificationsDropdown(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 3aeeedbb45d..5cfe8723204 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,7 +1,9 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new + initBoards(); }); diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js index 7889704a324..cd923f13ce8 100644 --- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -1,8 +1,10 @@ import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; document.addEventListener('DOMContentLoaded', () => { new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + initPipelines(); }); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 460a54ab504..1aeed197385 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import initNotes from '~/init_notes'; import initChangesDropdown from '~/init_changes_dropdown'; +import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import { fetchCommitMergeRequests } from '~/commit_merge_requests'; document.addEventListener('DOMContentLoaded', () => { @@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => { initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); fetchCommitMergeRequests(); + initDiffNotes(); }); diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js index 890062eeee6..d1c78bd61db 100644 --- a/app/assets/javascripts/pages/projects/compare/index.js +++ b/app/assets/javascripts/pages/projects/compare/index.js @@ -1,5 +1,3 @@ import initCompareAutocomplete from '~/compare_autocomplete'; -export default () => { - initCompareAutocomplete(); -}; +document.addEventListener('DOMContentLoaded', initCompareAutocomplete); diff --git a/app/assets/javascripts/pages/projects/environments/folder/index.js b/app/assets/javascripts/pages/projects/environments/folder/index.js new file mode 100644 index 00000000000..5feaf944038 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/folder/index.js @@ -0,0 +1,3 @@ +import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle'; + +document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 9b1d52692a3..de1e13de7e9 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,7 @@ import Project from './project'; import ShortcutsNavigation from '../../shortcuts_navigation'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index 1e56aa58da2..3950fe0a1fd 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,4 +1,5 @@ import initIssuableSidebar from '~/init_issuable_sidebar'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; import Issue from '~/issue'; import ShortcutsIssuable from '~/shortcuts_issuable'; import ZenMode from '~/zen_mode'; @@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => { new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new initIssuableSidebar(); + initSidebarBundle(); }); diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js new file mode 100644 index 00000000000..b92aef53510 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js @@ -0,0 +1,3 @@ +import initSidebarBundle from '~/sidebar/sidebar_bundle'; + +document.addEventListener('DOMContentLoaded', initSidebarBundle); diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/show/index.js new file mode 100644 index 00000000000..b92aef53510 --- /dev/null +++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/show/index.js @@ -0,0 +1,3 @@ +import initSidebarBundle from '~/sidebar/sidebar_bundle'; + +document.addEventListener('DOMContentLoaded', initSidebarBundle); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index 1d5aec4001d..6c9afddefac 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,5 +1,6 @@ import Compare from '~/compare'; import MergeRequest from '~/merge_request'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; document.addEventListener('DOMContentLoaded', () => { const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); @@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => { new MergeRequest({ // eslint-disable-line no-new action: mrNewSubmitNode.dataset.mrSubmitAction, }); + initPipelines(); } }); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 07f3e579c97..b7b6b0b5364 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -3,18 +3,23 @@ import ZenMode from '~/zen_mode'; import initNotes from '~/init_notes'; import initIssuableSidebar from '~/init_issuable_sidebar'; import initDiffNotes from '~/diff_notes/diff_notes_bundle'; +import initSidebarBundle from '~/sidebar/sidebar_bundle'; import ShortcutsIssuable from '~/shortcuts_issuable'; import Diff from '~/diff'; import { handleLocationHash } from '~/lib/utils/common_utils'; import howToMerge from '~/how_to_merge'; +import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import initWidget from '../../../../vue_merge_request_widget'; document.addEventListener('DOMContentLoaded', () => { new Diff(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new initIssuableSidebar(); + initSidebarBundle(); initNotes(); initDiffNotes(); + initPipelines(); const mrShowNode = document.querySelector('.merge-request'); @@ -25,4 +30,5 @@ document.addEventListener('DOMContentLoaded', () => { new ShortcutsIssuable(true); // eslint-disable-line no-new handleLocationHash(); howToMerge(); + initWidget(); }); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index a3fd22aff2a..7354243e4c8 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ -import BranchGraph from './branch_graph'; +import BranchGraph from '../../../network/branch_graph'; export default (function() { function Network(opts) { diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js new file mode 100644 index 00000000000..e7dfd2d0128 --- /dev/null +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -0,0 +1,16 @@ +import ShortcutsNetwork from '../../../../shortcuts_network'; +import Network from '../network'; + +document.addEventListener('DOMContentLoaded', () => { + if (!$('.network-graph').length) return; + + const networkGraph = new Network({ + url: $('.network-graph').attr('data-url'), + commit_url: $('.network-graph').attr('data-commit-url'), + ref: $('.network-graph').attr('data-ref'), + commit_id: $('.network-graph').attr('data-commit-id'), + }); + + // eslint-disable-next-line no-new + new ShortcutsNetwork(networkGraph.branch_graph); +}); diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index 71c49deb9d0..ea6fd961393 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new ProjectNew(); // eslint-disable-line no-new initProjectVisibilitySelector(); initProjectNew.bindEvents(); -}; +}); diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index ab5596e70f0..25dfa99ad9c 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; -import PipelinesStore from './stores/pipelines_store'; -import pipelinesComponent from './components/pipelines.vue'; -import Translate from '../vue_shared/translate'; +import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; +import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; +import Translate from '../../../../vue_shared/translate'; Vue.use(Translate); diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index eb14c7a0e78..b9f8707fd6e 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index e2285494e62..47736fc5f42 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import Vue from 'vue'; import VueResource from 'vue-resource'; +import '../../vue_shared/vue_resource_interceptor'; Vue.use(VueResource); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 689befc742e..14545824e74 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { super(); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); - this.editBtn = document.querySelector('.js-issuable-edit'); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); - Mousetrap.bind('e', this.editIssue.bind(this)); + Mousetrap.bind('e', ShortcutsIssuable.editIssue); if (isMergeRequest) { this.enabledHelp.push('.hidden-shortcut.merge_requests'); @@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { return false; } - editIssue() { + static editIssue() { // Need to click the element as on issues, editing is inline // on merge request, editing is on a different page - this.editBtn.click(); + document.querySelector('.js-issuable-edit').click(); return false; } diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 04c39d7b6b5..377846db70e 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,13 +1,9 @@ import Mediator from './sidebar_mediator'; import { mountSidebar, getSidebarOptions } from './mount_sidebar'; -function domContentLoaded() { +export default () => { const mediator = new Mediator(getSidebarOptions()); mediator.fetch(); mountSidebar(mediator); -} - -document.addEventListener('DOMContentLoaded', domContentLoaded); - -export default domContentLoaded; +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 109a302a172..54a98abf860 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,8 +1,8 @@ <script> /* eslint-disable vue/require-default-prop */ - import pipelineStage from '../../pipelines/components/stage.vue'; - import ciIcon from '../../vue_shared/components/ci_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; + import pipelineStage from '~/pipelines/components/stage.vue'; + import ciIcon from '~/vue_shared/components/ci_icon.vue'; + import icon from '~/vue_shared/components/icon.vue'; export default { name: 'MRWidgetPipeline', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 7ba6c29006a..162f048aac7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -227,7 +227,8 @@ export default { @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" - type="button"> + type="button" + class="qa-merge-button"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index e9f23b0b113..143fd328d88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children" > <button type="button" - class="btn btn-sm btn-reopen btn-success" + class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" :disabled="isMakingRequest" @click="rebase" > diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 6b9918b65b0..69a9132a2da 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; const vm = new Vue(mrWidgetOptions); @@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => { window.gl.mrWidget = { checkStatus: vm.checkStatus, }; -}); +}; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 17801ed5910..8b680c2dc52 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -196,17 +196,9 @@ @media (min-width: $screen-sm-min) { font-size: 0; - div { - display: inline; - } - .fa-spinner { font-size: 12px; } - - span { - font-size: 6px; - } } .ci-status-link { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6763af4e98b..b9390450477 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -13,10 +13,20 @@ display: inline-block; } + .issuable-meta { + .author_link { + display: inline-block; + } + + .issuable-comments { + height: 18px; + } + } + .icon-merge-request-unmerged { height: 13px; margin-bottom: 3px; - } + } } } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index a94726887d9..cc38608eda5 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController def members_update member_params = params.permit(:user_ids, :access_level, :expires_at) - result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute + result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group) if result[:status] == :success redirect_to [:admin, @group], notice: 'Users were successfully added.' diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index c6b1e443de6..7a6a00b8e13 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -3,20 +3,31 @@ module MembershipActions def create create_params = params.permit(:user_ids, :access_level, :expires_at) - result = Members::CreateService.new(membershipable, current_user, create_params).execute - - redirect_url = members_page_url + result = Members::CreateService.new(current_user, create_params).execute(membershipable) if result[:status] == :success - redirect_to redirect_url, notice: 'Users were successfully added.' + redirect_to members_page_url, notice: 'Users were successfully added.' else - redirect_to redirect_url, alert: result[:message] + redirect_to members_page_url, alert: result[:message] + end + end + + def update + update_params = params.require(root_params_key).permit(:access_level, :expires_at) + member = membershipable.members_and_requesters.find(params[:id]) + member = Members::UpdateService + .new(current_user, update_params) + .execute(member) + .present(current_user: current_user) + + respond_to do |format| + format.js { render 'shared/members/update', locals: { member: member } } end end def destroy - Members::DestroyService.new(membershipable, current_user, params) - .execute(:all) + member = membershipable.members_and_requesters.find(params[:id]) + Members::DestroyService.new(current_user).execute(member) respond_to do |format| format.html do @@ -36,14 +47,17 @@ module MembershipActions end def approve_access_request - Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute + access_requester = membershipable.requesters.find(params[:id]) + Members::ApproveAccessRequestService + .new(current_user, params) + .execute(access_requester) redirect_to members_page_url end def leave - member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id) - .execute(:all) + member = membershipable.members_and_requesters.find_by!(user_id: current_user.id) + Members::DestroyService.new(current_user).execute(member) notice = if member.request? @@ -62,17 +76,43 @@ module MembershipActions end end + def resend_invite + member = membershipable.members.find(params[:id]) + + if member.invite? + member.resend_invite + + redirect_to members_page_url, notice: 'The invitation was successfully resent.' + else + redirect_to members_page_url, alert: 'The invitation has already been accepted.' + end + end + protected def membershipable raise NotImplementedError end + def root_params_key + case membershipable + when Namespace + :group_member + when Project + :project_member + else + raise "Unknown membershipable type: #{membershipable}!" + end + end + def members_page_url - if membershipable.is_a?(Project) + case membershipable + when Namespace + polymorphic_url([membershipable, :members]) + when Project project_project_members_path(membershipable) else - polymorphic_url([membershipable, :members]) + raise "Unknown membershipable type: #{membershipable}!" end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 4a2bfc1f887..9f3bb60b4cc 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute end - def group_merge_requests - @group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute - end - def authorize_admin_group! unless can?(current_user, :admin_group, group) return render_404 diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 2c371e76313..f210434b2d7 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController @group_member = @group.group_members.new end - def update - @group_member = @group.members_and_requesters.find(params[:id]) - .present(current_user: current_user) - - return render_403 unless can?(current_user, :update_group_member, @group_member) - - @group_member.update_attributes(member_params) - end - - def resend_invite - redirect_path = group_group_members_path(@group) - - @group_member = @group.group_members.find(params[:id]) - - if @group_member.invite? - @group_member.resend_invite - - redirect_to redirect_path, notice: 'The invitation was successfully resent.' - else - redirect_to redirect_path, alert: 'The invitation has already been accepted.' - end - end - - protected - - def member_params - params.require(:group_member).permit(:access_level, :user_id, :expires_at) - end - # MembershipActions concern alias_method :membershipable, :group end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 14b9d6c22bd..283c3e5f1e0 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController before_action :authorize_create_group!, only: [:new] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] - before_action :group_merge_requests, only: [:merge_requests] before_action :event_filter, only: [:activity] before_action :user_actions, only: [:show, :subgroups] diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d7372beb9d3..e9b4679f94c 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_member = @project.project_members.new end - def update - @project_member = @project.members_and_requesters.find(params[:id]) - .present(current_user: current_user) - - return render_403 unless can?(current_user, :update_project_member, @project_member) - - @project_member.update_attributes(member_params) - end - - def resend_invite - redirect_path = project_project_members_path(@project) - - @project_member = @project.project_members.find(params[:id]) - - if @project_member.invite? - @project_member.resend_invite - - redirect_to redirect_path, notice: 'The invitation was successfully resent.' - else - redirect_to redirect_path, alert: 'The invitation has already been accepted.' - end - end - def import @projects = current_user.authorized_projects.order_id_desc end @@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController notice: notice) end - protected - - def member_params - params.require(:project_member).permit(:user_id, :access_level, :expires_at) - end - # MembershipActions concern alias_method :membershipable, :project end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index f013e177c5b..5c9fce211ec 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder end end elsif only_group_labels? - label_ids << Label.where(group_id: group.id) + label_ids << Label.where(group_id: group_ids) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group - strong_memoize(:group) do + def group_ids + strong_memoize(:group_ids) do group = Group.find(params[:group_id]) - authorized_to_read_labels?(group) && group + groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group] + groups_user_can_read_labels(groups).map(&:id) end end @@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder Ability.allowed?(current_user, :read_label, label_parent) end + + def groups_user_can_read_labels(groups) + DeclarativePolicy.user_scope do + groups.select { |group| authorized_to_read_labels?(group) } + end + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a6e1de6ffdc..0e806d16bc5 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -12,75 +12,42 @@ module BlobHelper def edit_blob_path(project = @project, ref = @ref, path = @path, options = {}) project_edit_blob_path(project, - tree_join(ref, path), - options[:link_opts]) - end - - def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil - - return unless blob && blob.readable_text? - - common_classes = "btn js-edit-blob #{options[:extra_class]}" - - if !on_top_of_branch?(project, ref) - button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) - link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: edit_blob_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag 'Edit', - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: 'edit', fork_path: fork_path } - end + tree_join(ref, path), + options[:link_opts]) end def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" end - def ide_edit_text - "#{_('Web IDE')}" - end + def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) + return unless blob = readable_blob(options, path, project, ref) - def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) - return unless show_new_ide? + common_classes = "btn js-edit-blob #{options[:extra_class]}" - blob = options.delete(:blob) - blob ||= project.repository.blob_at(ref, path) rescue nil + edit_button_tag(blob, + common_classes, + _('Edit'), + edit_blob_path(project, ref, path, options), + project, + ref) + end - return unless blob && blob.readable_text? + def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) + return unless show_new_ide? + return unless blob = readable_blob(options, path, project, ref) common_classes = "btn js-edit-ide #{options[:extra_class]}" - if !on_top_of_branch?(project, ref) - button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' } - # This condition applies to anonymous or users who can edit directly - elsif current_user && can_modify_blob?(blob, project, ref) - link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" - elsif current_user && can?(current_user, :fork_project, project) - continue_params = { - to: ide_edit_path(project, ref, path, options), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag ide_edit_text, - class: common_classes, - data: { fork_path: fork_path } - end + edit_button_tag(blob, + common_classes, + _('Web IDE'), + ide_edit_path(project, ref, path, options), + project, + ref) end - def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) + def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user blob = project.repository.blob_at(ref, path) rescue nil @@ -96,21 +63,12 @@ module BlobHelper elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) - continue_params = { - to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to #{action} this file again.", - notice_now: edit_in_new_fork_notice_now - } - fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params) - - button_tag label, - class: "#{common_classes} js-edit-blob-link-fork-toggler", - data: { action: action, fork_path: fork_path } + edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action) end end def replace_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -122,7 +80,7 @@ module BlobHelper end def delete_blob_link(project = @project, ref = @ref, path = @path) - modify_file_link( + modify_file_button( project, ref, path, @@ -332,4 +290,55 @@ module BlobHelper options end + + def readable_blob(options, path, project, ref) + blob = options.delete(:blob) + blob ||= project.repository.blob_at(ref, path) rescue nil + + blob if blob&.readable_text? + end + + def edit_blob_fork_params(path) + { + to: path, + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_modify_file_fork_params(action) + { + to: request.fullpath, + notice: edit_in_new_fork_notice_action(action), + notice_now: edit_in_new_fork_notice_now + } + end + + def edit_fork_button_tag(common_classes, project, label, params, action = 'edit') + fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params) + + button_tag label, + class: "#{common_classes} js-edit-blob-link-fork-toggler", + data: { action: action, fork_path: fork_path } + end + + def edit_disabled_button_tag(button_text, common_classes) + button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) + end + + def edit_link_tag(link_text, edit_path, common_classes) + link_to link_text, edit_path, class: "#{common_classes} btn-sm" + end + + def edit_button_tag(blob, common_classes, text, edit_path, project, ref) + if !on_top_of_branch?(project, ref) + edit_disabled_button_tag(text, common_classes) + # This condition only applies to users who are logged in + # Web IDE (Beta) requires the user to have this feature enabled + elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) + edit_link_tag(text, edit_path, common_classes) + elsif current_user && can?(current_user, :fork_project, project) + edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path)) + end + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 5fbaa17c40e..7910de73c52 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -19,6 +19,20 @@ module GroupsHelper can?(current_user, :change_share_with_group_lock, group) end + def group_issues_count(state:) + IssuesFinder + .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) + .execute + .count + end + + def group_merge_requests_count(state:) + MergeRequestsFinder + .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) + .execute + .count + end + def group_icon(group, options = {}) img_path = group_icon_url(group, options) image_tag img_path, options @@ -77,10 +91,6 @@ module GroupsHelper end end - def group_issues(group) - IssuesFinder.new(current_user, group_id: group.id).execute - end - def remove_group_message(group) _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % { group_name: group.name } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7cd84fe69c9..44ecc2212f2 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -234,7 +234,7 @@ module IssuablesHelper data.merge!(updated_at_by(issuable)) - data.to_json + data end def updated_at_by(issuable) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f5733b4b57c..f6a6d9bebde 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -83,6 +83,10 @@ module TreeHelper " A fork of this project has been created that you can make changes in, so you can submit a merge request." end + def edit_in_new_fork_notice_action(action) + edit_in_new_fork_notice + " Try to #{action} this file again." + end + def commit_in_fork_help "A new branch will be created in your fork and a new merge request will be started." end diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb index f321db75eeb..fbd0f123341 100644 --- a/app/models/chat_name.rb +++ b/app/models/chat_name.rb @@ -1,4 +1,6 @@ class ChatName < ActiveRecord::Base + LAST_USED_AT_INTERVAL = 1.hour + belongs_to :service belongs_to :user @@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base validates :user_id, uniqueness: { scope: [:service_id] } validates :chat_id, uniqueness: { scope: [:service_id, :team_id] } + + # Updates the "last_used_timestamp" but only if it wasn't already updated + # recently. + # + # The throttling this method uses is put in place to ensure that high chat + # traffic doesn't result in many UPDATE queries being performed. + def update_last_used_at + return unless update_last_used_at? + + obtained = Gitlab::ExclusiveLease + .new("chat_name/last_used_at/#{id}", timeout: LAST_USED_AT_INTERVAL.to_i) + .try_obtain + + touch(:last_used_at) if obtained + end + + def update_last_used_at? + last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago + end end diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index afeae69ba39..1dd0e050ba9 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -6,7 +6,10 @@ module Ci belongs_to :group - validates :key, uniqueness: { scope: :group_id } + validates :key, uniqueness: { + scope: :group_id, + message: "(%{value}) has already been taken" + } scope :unprotected, -> { where(protected: false) } end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 67d3ec81b6f..7c71291de84 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -6,7 +6,10 @@ module Ci belongs_to :project - validates :key, uniqueness: { scope: [:project_id, :environment_scope] } + validates :key, uniqueness: { + scope: [:project_id, :environment_scope], + message: "(%{value}) has already been taken" + } scope :unprotected, -> { where(protected: false) } end diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb index 62bc6b809f4..d502e7e54c6 100644 --- a/app/models/concerns/access_requestable.rb +++ b/app/models/concerns/access_requestable.rb @@ -8,6 +8,6 @@ module AccessRequestable extend ActiveSupport::Concern def request_access(user) - Members::RequestAccessService.new(self, user).execute + Members::RequestAccessService.new(user).execute(self) end end diff --git a/app/models/member.rb b/app/models/member.rb index 2d17795e62d..408e8b2d704 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -128,7 +128,7 @@ class Member < ActiveRecord::Base find_by(invite_token: invite_token) end - def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil) + def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false) # `user` can be either a User object, User ID or an email to be invited member = retrieve_member(source, user, existing_members) access_level = retrieve_access_level(access_level) @@ -143,11 +143,13 @@ class Member < ActiveRecord::Base if member.request? ::Members::ApproveAccessRequestService.new( - source, current_user, - id: member.id, access_level: access_level - ).execute + ).execute( + member, + skip_authorization: ldap, + skip_log_audit_event: ldap + ) else member.save end diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index eb4da68bb7e..37ea45109ae 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -30,10 +30,10 @@ class SlashCommandsService < Service def trigger(params) return unless valid_token?(params[:token]) - user = find_chat_user(params) + chat_user = find_chat_user(params) - if user - Gitlab::SlashCommands::Command.new(project, user, params).execute + if chat_user&.user + Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else url = authorize_chat_name_url(params) Gitlab::SlashCommands::Presenters::Access.new(url).authorize diff --git a/app/models/tree.rb b/app/models/tree.rb index c89b8eca9be..4c1856b67a8 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -9,10 +9,9 @@ class Tree @repository = repository @sha = sha @path = path - @recursive = recursive git_repo = @repository.raw_repository - @entries = get_entries(git_repo, @sha, @path, recursive: @recursive) + @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) end def readme @@ -58,21 +57,4 @@ class Tree def sorted_entries trees + blobs + submodules end - - private - - def get_entries(git_repo, sha, path, recursive: false) - current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path) - ordered_entries = [] - - current_path_entries.each do |entry| - ordered_entries << entry - - if recursive && entry.dir? - ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true)) - end - end - - ordered_entries - end end diff --git a/app/models/user.rb b/app/models/user.rb index 8610ca27b7f..982080763d2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -431,7 +431,7 @@ class User < ActiveRecord::Base end def self.non_internal - where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)]) + where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND ")) end # diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 4f5c5567b42..d458b814183 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -9,8 +9,8 @@ module ChatNames chat_name = find_chat_name return unless chat_name - chat_name.touch(:last_used_at) - chat_name.user + chat_name.update_last_used_at + chat_name end private diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb index 280a2c3afa4..ffde824972c 100644 --- a/app/services/ci/create_trace_artifact_service.rb +++ b/app/services/ci/create_trace_artifact_service.rb @@ -4,13 +4,33 @@ module Ci return if job.job_artifacts_trace job.trace.read do |stream| - if stream.file? - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) + break unless stream.file? + + clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| + create_job_trace!(job, clone_path) + FileUtils.rm(stream.path) end end end + + private + + def create_job_trace!(job, path) + File.open(path) do |stream| + job.create_job_artifacts_trace!( + project: job.project, + file_type: :trace, + file: stream) + end + end + + def clone_file!(src_path, temp_dir) + FileUtils.mkdir_p(temp_dir) + Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| + temp_path = File.join(dir_path, "job.log") + FileUtils.copy(src_path, temp_path) + yield(temp_path) + end + end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 66a9b1f82e0..e87fd49d193 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -77,8 +77,12 @@ class IssuableBaseService < BaseService return unless labels params[:label_ids] = labels.split(",").map do |label_name| - service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) - label = service.execute + label = Labels::FindOrCreateService.new( + current_user, + parent, + title: label_name.strip, + available_labels: available_labels + ).execute label.try(:id) end.compact @@ -102,7 +106,7 @@ class IssuableBaseService < BaseService end def available_labels - LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end def merge_quick_actions_into_params!(issuable) @@ -303,4 +307,8 @@ class IssuableBaseService < BaseService def update_project_counter_caches?(issuable) issuable.state_changed? end + + def parent + project + end end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 940c8b333d3..079f611b3f3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -1,8 +1,9 @@ module Labels class FindOrCreateService - def initialize(current_user, project, params = {}) + def initialize(current_user, parent, params = {}) @current_user = current_user - @project = project + @parent = parent + @available_labels = params.delete(:available_labels) @params = params.dup.with_indifferent_access end @@ -13,12 +14,13 @@ module Labels private - attr_reader :current_user, :project, :params, :skip_authorization + attr_reader :current_user, :parent, :params, :skip_authorization def available_labels @available_labels ||= LabelsFinder.new( current_user, - project_id: project.id + "#{parent_type}_id".to_sym => parent.id, + only_group_labels: parent_is_group? ).execute(skip_authorization: skip_authorization) end @@ -27,8 +29,8 @@ module Labels def find_or_create_label new_label = available_labels.find_by(title: title) - if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = Labels::CreateService.new(params).execute(project: project) + if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) + new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent) end new_label @@ -37,5 +39,13 @@ module Labels def title params[:title] || params[:name] end + + def parent_type + parent.model_name.param_key + end + + def parent_is_group? + parent_type == "group" + end end end diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index 2a2bb0cae5b..6be08b590bc 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -1,51 +1,20 @@ module Members - class ApproveAccessRequestService < BaseService - include MembersHelper - - attr_accessor :source - - # source - The source object that respond to `#requesters` (i.g. project or group) - # current_user - The user that performs the access request approval - # params - A hash of parameters - # :user_id - User ID used to retrieve the access requester - # :id - Member ID used to retrieve the access requester - # :access_level - Optional access level set when the request is accepted - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params.slice(:user_id, :id, :access_level) - end - - # opts - A hash of options - # :force - Bypass permission check: current_user can be nil in that case - def execute(opts = {}) - condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } - access_requester = source.requesters.find_by!(condition) - - raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts) + class ApproveAccessRequestService < Members::BaseService + def execute(access_requester, skip_authorization: false, skip_log_audit_event: false) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester) access_requester.access_level = params[:access_level] if params[:access_level] access_requester.accept_request + after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event) + access_requester end private - def can_update_access_requester?(access_requester, opts = {}) - access_requester && ( - opts[:force] || - can?(current_user, update_member_permission(access_requester), access_requester) - ) - end - - def update_member_permission(member) - case member - when GroupMember - :update_group_member - when ProjectMember - :update_project_member - end + def can_update_access_requester?(access_requester) + can?(current_user, update_member_permission(access_requester), access_requester) end end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb deleted file mode 100644 index 2e89f00dad8..00000000000 --- a/app/services/members/authorized_destroy_service.rb +++ /dev/null @@ -1,61 +0,0 @@ -module Members - class AuthorizedDestroyService < BaseService - attr_accessor :member, :user - - def initialize(member, user = nil) - @member, @user = member, user - end - - def execute - return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - - Member.transaction do - unassign_issues_and_merge_requests(member) unless member.invite? - member.notification_setting&.destroy - - member.destroy - end - - if member.request? && member.user != user - notification_service.decline_access_request(member) - end - - member - end - - private - - def unassign_issues_and_merge_requests(member) - if member.is_a?(GroupMember) - issues = Issue.unscoped.select(1) - .joins(:project) - .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id) - .execute - .update_all(assignee_id: nil) - else - project = member.source - - # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X - issues = Issue.unscoped.select(1) - .where('issues.id = issue_assignees.issue_id') - .where(project_id: project.id) - - # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) - IssueAssignee.unscoped - .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) - .delete_all - - project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) - end - - member.user.invalidate_cache_counts - end - end -end diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb new file mode 100644 index 00000000000..74556fb20cf --- /dev/null +++ b/app/services/members/base_service.rb @@ -0,0 +1,49 @@ +module Members + class BaseService < ::BaseService + # current_user - The user that performs the action + # params - A hash of parameters + def initialize(current_user = nil, params = {}) + @current_user = current_user + @params = params + end + + def after_execute(args) + # overriden in EE::Members modules + end + + private + + def update_member_permission(member) + case member + when GroupMember + :update_group_member + when ProjectMember + :update_project_member + else + raise "Unknown member type: #{member}!" + end + end + + def override_member_permission(member) + case member + when GroupMember + :override_group_member + when ProjectMember + :override_project_member + else + raise "Unknown member type: #{member}!" + end + end + + def action_member_permission(action, member) + case action + when :update + update_member_permission(member) + when :override + override_member_permission(member) + else + raise "Unknown action '#{action}' on #{member}!" + end + end + end +end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 26906ae7167..bc6a9405aac 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -1,15 +1,8 @@ module Members - class CreateService < BaseService + class CreateService < Members::BaseService DEFAULT_LIMIT = 100 - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params - @error = nil - end - - def execute + def execute(source) return error('No users specified.') if params[:user_ids].blank? user_ids = params[:user_ids].split(',').uniq @@ -17,13 +10,15 @@ module Members return error("Too many users specified (limit is #{user_limit})") if user_limit && user_ids.size > user_limit - @source.add_users( + members = source.add_users( user_ids, params[:access_level], expires_at: params[:expires_at], current_user: current_user ) + members.each { |member| after_execute(member: member) } + success end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 05b93ac8fdb..b141bfd5fbc 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -1,42 +1,30 @@ module Members - class DestroyService < BaseService - include MembersHelper + class DestroyService < Members::BaseService + def execute(member, skip_authorization: false) + raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) - attr_accessor :source + return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) - ALLOWED_SCOPES = %i[members requesters all].freeze + Member.transaction do + unassign_issues_and_merge_requests(member) unless member.invite? + member.notification_setting&.destroy - def initialize(source, current_user, params = {}) - @source = source - @current_user = current_user - @params = params - end - - def execute(scope = :members) - raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) + member.destroy + end - member = find_member!(scope) + if member.request? && member.user != current_user + notification_service.decline_access_request(member) + end - raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) + after_execute(member: member) - AuthorizedDestroyService.new(member, current_user).execute + member end private - def find_member!(scope) - condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } - case scope - when :all - source.members.find_by(condition) || - source.requesters.find_by!(condition) - else - source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend - end - end - def can_destroy_member?(member) - member && can?(current_user, destroy_member_permission(member), member) + can?(current_user, destroy_member_permission(member), member) end def destroy_member_permission(member) @@ -45,7 +33,42 @@ module Members :destroy_group_member when ProjectMember :destroy_project_member + else + raise "Unknown member type: #{member}!" end end + + def unassign_issues_and_merge_requests(member) + if member.is_a?(GroupMember) + issues = Issue.unscoped.select(1) + .joins(:project) + .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) + + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all + + MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id) + .execute + .update_all(assignee_id: nil) + else + project = member.source + + # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X + issues = Issue.unscoped.select(1) + .where('issues.id = issue_assignees.issue_id') + .where(project_id: project.id) + + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped + .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues) + .delete_all + + project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) + end + + member.user.invalidate_cache_counts + end end end diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb index 2614153d900..24293b30005 100644 --- a/app/services/members/request_access_service.rb +++ b/app/services/members/request_access_service.rb @@ -1,13 +1,6 @@ module Members - class RequestAccessService < BaseService - attr_accessor :source - - def initialize(source, current_user) - @source = source - @current_user = current_user - end - - def execute + class RequestAccessService < Members::BaseService + def execute(source) raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) source.members.create( @@ -19,7 +12,7 @@ module Members private def can_request_access?(source) - source && can?(current_user, :request_access, source) + can?(current_user, :request_access, source) end end end diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb new file mode 100644 index 00000000000..48b3d59f7bd --- /dev/null +++ b/app/services/members/update_service.rb @@ -0,0 +1,16 @@ +module Members + class UpdateService < Members::BaseService + # returns the updated member + def execute(member, permission: :update) + raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member) + + old_access_level = member.human_access + + if member.update_attributes(params) + after_execute(action: permission, old_access_level: old_access_level, member: member) + end + + member + end + end +end diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb index 4bfa3c45303..72660be6c43 100644 --- a/app/validators/variable_duplicates_validator.rb +++ b/app/validators/variable_duplicates_validator.rb @@ -5,6 +5,8 @@ # - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model class VariableDuplicatesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) + return if record.errors.include?(:"#{attribute}.key") + if options[:scope] scoped = value.group_by do |variable| Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml deleted file mode 100644 index 9d05bff6c4e..00000000000 --- a/app/views/groups/group_members/update.js.haml +++ /dev/null @@ -1,4 +0,0 @@ -:plain - var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}'); - $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); - gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}")); diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index f2ae7c52031..ca3f018c5e6 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,12 +1,13 @@ - page_title "Issues" -- group_issues_exists = group_issues(@group).exists? = content_for :meta_tags do = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' -- if group_issues_exists +- if group_issues_count(state: 'all').zero? + = render 'shared/empty_states/issues', project_select_button: true +- else .top-area = render 'shared/issuable/nav', type: :issues .nav-controls @@ -19,5 +20,3 @@ = render 'shared/issuable/search_bar', type: :issues = render 'shared/issues' -- else - = render 'shared/empty_states/issues', project_select_button: true diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 046b92bd9fb..520fe217ae1 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -3,7 +3,7 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' -- if @group_merge_requests.empty? +- if group_merge_requests_count(state: 'all').zero? = render 'shared/empty_states/merge_requests', project_select_button: true - else .top-area diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 47ae79b7a69..b520f28123f 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,6 +1,5 @@ -- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count -- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count - +- issues_count = group_issues_count(state: 'opened') +- merge_requests_count = group_merge_requests_count(state: 'opened') - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index 8129c72feb2..f455522d17c 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -3,7 +3,7 @@ .radio = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio" + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" %strong Fast-forward merge %br %span.descr diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 2a77dedd9a2..1b150ec3e5c 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -11,8 +11,8 @@ = view_on_environment_button(@commit.sha, @path, @environment) if @environment .btn-group{ role: "group" }< - = edit_blob_link - = ide_blob_link + = edit_blob_button + = ide_edit_button - if current_user = replace_blob_link = delete_blob_link diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index b3afd16f900..f1324c61500 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -27,6 +27,3 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 = commit_in_fork_help - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('blob') diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 3f699882c5f..a3fed25af28 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -9,4 +9,3 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('commit_pipelines') diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 4058e61eb9a..ba86d943967 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -8,7 +8,6 @@ - page_description @commit.description - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('diff_notes') .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 6ff7bcae54f..078bd0eee63 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -20,7 +20,7 @@ .avatar-cell.hidden-xs = author_avatar(commit, size: 36) - .commit-detail + .commit-detail.flex-list .commit-content = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") %span.commit-row-message.visible-xs-inline diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0b01e38d23d..47bfcb21cf4 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -17,7 +17,7 @@ \ - if editable_diff?(diff_file) - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} - = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, + = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) - if image_diff && image_replaced diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0931ceb1512..b947b91322d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -85,7 +85,7 @@ .settings-content = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = render 'merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" = render 'export', project: @project diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index eca10d99908..d8054dbc372 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -3,7 +3,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag("environments_folder") #environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), "folder-name" => @folder, diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 7bc5c46d64a..d63443c9da5 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -55,7 +55,7 @@ .issue-details.issuable-details .detail-page-description.content-block - %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue) + %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json #js-issuable-app %h2.title= markdown_field(@issue, :title) - if @issue.description.present? diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index e29f21b3bec..7cbc984ef19 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -23,9 +23,6 @@ #js-vue-mr-widget.mr-widget - - content_for :page_specific_javascripts do - = webpack_bundle_tag 'vue_merge_request_widget' - .content-block.content-block-small.emoji-list-container = render 'award_emoji/awards_block', awardable: @merge_request, inline: true diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 97be8950db0..4b7be9a223f 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,5 @@ - breadcrumb_title "Graph" - page_title "Graph", @ref -- content_for :page_specific_javascripts do - = webpack_bundle_tag('network') = render "head" %div{ class: container_class } .project-network diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index fdcc60f48a5..cf95cdbfec2 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -12,6 +12,3 @@ "has-ci" => @repository.gitlab_ci_yml, "ci-lint-path" => ci_lint_path, "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } - - = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('pipelines') diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml deleted file mode 100644 index d15f4310ff5..00000000000 --- a/app/views/projects/project_members/update.js.haml +++ /dev/null @@ -1,4 +0,0 @@ -:plain - var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}'); - $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); - gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}")); diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml new file mode 100644 index 00000000000..2cc2a6b2b5b --- /dev/null +++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml @@ -0,0 +1,26 @@ +%h4 + = s_('PrometheusService|Auto configuration') + +- if service.manual_configuration? + .well + = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below') +- else + .container-fluid + .row + - if service.prometheus_installed? + .col-sm-2 + .svg-container + = image_tag 'illustrations/monitoring/getting_started.svg' + .col-sm-10 + %p.text-success.prepend-top-default + = s_('PrometheusService|Prometheus is being automatically managed on your clusters') + = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn' + - else + .col-sm-2 + = image_tag 'illustrations/monitoring/loading.svg' + .col-sm-10 + %p.prepend-top-default + = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments') + = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success' + +%hr diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml index 5e320a252d8..88acb824ba7 100644 --- a/app/views/projects/services/prometheus/_help.html.haml +++ b/app/views/projects/services/prometheus/_help.html.haml @@ -1,29 +1,5 @@ -%h4 - = s_('PrometheusService|Auto configuration') - -- if @service.manual_configuration? - .well - = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below') -- else - .container-fluid - .row - - if @service.prometheus_installed? - .col-sm-2 - .svg-container - = image_tag 'illustrations/monitoring/getting_started.svg' - .col-sm-10 - %p.text-success.prepend-top-default - = s_('PrometheusService|Prometheus is being automatically managed on your clusters') - = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn' - - else - .col-sm-2 - = image_tag 'illustrations/monitoring/loading.svg' - .col-sm-10 - %p.prepend-top-default - = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments') - = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success' - -%hr +- if @project + = render 'projects/services/prometheus/configuration_banner', project: @project, service: @service %h4.append-bottom-default = s_('PrometheusService|Manual configuration') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 05539dfed7c..39511435508 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -75,7 +75,7 @@ - if show_new_ide? = succeed " " do = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do - = ide_edit_text + = _('Web IDE') = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index a10fc42b82d..3312254f5fb 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -6,7 +6,6 @@ - content_for :page_specific_javascripts do = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'boards' %script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index dc583d3eb3b..c1589027898 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,7 +1,6 @@ - todo = issuable_todo(issuable) - content_for :page_specific_javascripts do = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('sidebar') %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } diff --git a/app/views/shared/members/update.js.haml b/app/views/shared/members/update.js.haml new file mode 100644 index 00000000000..55050bd8a15 --- /dev/null +++ b/app/views/shared/members/update.js.haml @@ -0,0 +1,6 @@ +- member = local_assigns.fetch(:member) + +:plain + var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}'); + $("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name')); + gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}")); diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 09559e3b696..d7e24491516 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -1,42 +1,10 @@ class AuthorizedProjectsWorker include ApplicationWorker + prepend WaitableWorker - # Schedules multiple jobs and waits for them to be completed. - def self.bulk_perform_and_wait(args_list) - # Short-circuit: it's more efficient to do small numbers of jobs inline - return bulk_perform_inline(args_list) if args_list.size <= 3 - - waiter = Gitlab::JobWaiter.new(args_list.size) - - # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] - # into [[1, "key"], [2, "key"], [3, "key"]] - waiting_args_list = args_list.map { |args| [*args, waiter.key] } - bulk_perform_async(waiting_args_list) - - waiter.wait - end - - # Performs multiple jobs directly. Failed jobs will be put into sidekiq so - # they can benefit from retries - def self.bulk_perform_inline(args_list) - failed = [] - - args_list.each do |args| - begin - new.perform(*args) - rescue - failed << args - end - end - - bulk_perform_async(failed) if failed.present? - end - - def perform(user_id, notify_key = nil) + def perform(user_id) user = User.find_by(id: user_id) user&.refresh_authorized_projects - ensure - Gitlab::JobWaiter.notify(notify_key, jid) if notify_key end end diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb new file mode 100644 index 00000000000..48ebe862248 --- /dev/null +++ b/app/workers/concerns/waitable_worker.rb @@ -0,0 +1,44 @@ +module WaitableWorker + extend ActiveSupport::Concern + + module ClassMethods + # Schedules multiple jobs and waits for them to be completed. + def bulk_perform_and_wait(args_list, timeout: 10) + # Short-circuit: it's more efficient to do small numbers of jobs inline + return bulk_perform_inline(args_list) if args_list.size <= 3 + + waiter = Gitlab::JobWaiter.new(args_list.size) + + # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]] + # into [[1, "key"], [2, "key"], [3, "key"]] + waiting_args_list = args_list.map { |args| [*args, waiter.key] } + bulk_perform_async(waiting_args_list) + + waiter.wait(timeout) + end + + # Performs multiple jobs directly. Failed jobs will be put into sidekiq so + # they can benefit from retries + def bulk_perform_inline(args_list) + failed = [] + + args_list.each do |args| + begin + new.perform(*args) + rescue + failed << args + end + end + + bulk_perform_async(failed) if failed.present? + end + end + + def perform(*args) + notify_key = args.pop if Gitlab::JobWaiter.key?(args.last) + + super(*args) + ensure + Gitlab::JobWaiter.notify(notify_key, jid) if notify_key + end +end diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index d80b3b15840..68960f72bf6 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker def perform Member.expired.find_each do |member| begin - Members::AuthorizedDestroyService.new(member).execute + Members::DestroyService.new.execute(member, skip_authorization: true) rescue => ex logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") end diff --git a/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml b/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml new file mode 100644 index 00000000000..b909bb2d021 --- /dev/null +++ b/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml @@ -0,0 +1,5 @@ +--- +title: Clear the Labels dropdown search filter after a selection is made +merge_request: 17393 +author: Andrew Torres +type: changed diff --git a/changelogs/unreleased/34130-null-pipes.yml b/changelogs/unreleased/34130-null-pipes.yml deleted file mode 100644 index a56e5cf8db2..00000000000 --- a/changelogs/unreleased/34130-null-pipes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent MR Widget error when no CI configured -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml b/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml new file mode 100644 index 00000000000..dddd8473df5 --- /dev/null +++ b/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml @@ -0,0 +1,5 @@ +--- +title: Keep link when redacting unauthorized object links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml b/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml deleted file mode 100644 index 27eee7d943b..00000000000 --- a/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve query performance of MembersFinder. -merge_request: 17190 -author: -type: performance diff --git a/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml b/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml deleted file mode 100644 index 507367c98c4..00000000000 --- a/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable Legacy Authorization by default on Cluster creations -merge_request: 17302 -author: -type: fixed diff --git a/changelogs/unreleased/41851-enable-eslint-codeclimate.yml b/changelogs/unreleased/41851-enable-eslint-codeclimate.yml new file mode 100644 index 00000000000..98924f3eae8 --- /dev/null +++ b/changelogs/unreleased/41851-enable-eslint-codeclimate.yml @@ -0,0 +1,5 @@ +--- +title: Enables eslint in codeclimate job +merge_request: 17392 +author: +type: other diff --git a/changelogs/unreleased/42877-snippets-dashboard-slow.yml b/changelogs/unreleased/42877-snippets-dashboard-slow.yml deleted file mode 100644 index 839b44ad272..00000000000 --- a/changelogs/unreleased/42877-snippets-dashboard-slow.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve query performance for snippets dashboard. -merge_request: 17088 -author: -type: performance diff --git a/changelogs/unreleased/43261-fix-prometheus-installation.yml b/changelogs/unreleased/43261-fix-prometheus-installation.yml new file mode 100644 index 00000000000..b5fc7980390 --- /dev/null +++ b/changelogs/unreleased/43261-fix-prometheus-installation.yml @@ -0,0 +1,5 @@ +--- +title: Allow Prometheus application to be installed from Cluster applications +merge_request: 17372 +author: +type: fixed diff --git a/changelogs/unreleased/43275-improve-variables-validation-message.yml b/changelogs/unreleased/43275-improve-variables-validation-message.yml new file mode 100644 index 00000000000..88ef93123a0 --- /dev/null +++ b/changelogs/unreleased/43275-improve-variables-validation-message.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicated error message on duplicate variable validation +merge_request: 17135 +author: +type: fixed diff --git a/changelogs/unreleased/43315-gpg-popover.yml b/changelogs/unreleased/43315-gpg-popover.yml new file mode 100644 index 00000000000..69238aa8075 --- /dev/null +++ b/changelogs/unreleased/43315-gpg-popover.yml @@ -0,0 +1,5 @@ +--- +title: Fixes gpg popover layout +merge_request: 17323 +author: +type: fixed diff --git a/changelogs/unreleased/43373-fix-cache-index-appending.yml b/changelogs/unreleased/43373-fix-cache-index-appending.yml deleted file mode 100644 index fdb293ea04d..00000000000 --- a/changelogs/unreleased/43373-fix-cache-index-appending.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix issue with cache key being empty when variable used as the key -merge_request: 17260 -author: -type: fixed diff --git a/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml b/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml new file mode 100644 index 00000000000..e163c04f430 --- /dev/null +++ b/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml @@ -0,0 +1,6 @@ +--- +title: Ensure group issues and merge requests pages show results from subgroups when + there are no results from the current group +merge_request: 17312 +author: +type: fixed diff --git a/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml b/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml new file mode 100644 index 00000000000..25bcbf2fbab --- /dev/null +++ b/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml @@ -0,0 +1,5 @@ +--- +title: Fixes Prometheus admin configuration page +merge_request: 17377 +author: +type: fixed diff --git a/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml b/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml new file mode 100644 index 00000000000..bda4ec84e5c --- /dev/null +++ b/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml @@ -0,0 +1,5 @@ +--- +title: Fix Group labels load failure when there are duplicate labels present +merge_request: 17353 +author: +type: fixed diff --git a/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml b/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml new file mode 100644 index 00000000000..28820649af3 --- /dev/null +++ b/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml @@ -0,0 +1,5 @@ +--- +title: Add catch-up background migration to migrate pipeline stages +merge_request: 15741 +author: +type: performance diff --git a/changelogs/unreleased/fix-500-for-invalid-upload-path.yml b/changelogs/unreleased/fix-500-for-invalid-upload-path.yml deleted file mode 100644 index a4ce00c64c4..00000000000 --- a/changelogs/unreleased/fix-500-for-invalid-upload-path.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 error when loading an invalid upload URL -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/flipper-caching.yml b/changelogs/unreleased/flipper-caching.yml deleted file mode 100644 index 6db27fd579e..00000000000 --- a/changelogs/unreleased/flipper-caching.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Increase feature flag cache TTL to one hour -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/grpc-unavailable-restart.yml b/changelogs/unreleased/grpc-unavailable-restart.yml new file mode 100644 index 00000000000..5ce08d66004 --- /dev/null +++ b/changelogs/unreleased/grpc-unavailable-restart.yml @@ -0,0 +1,5 @@ +--- +title: Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed +merge_request: 17293 +author: +type: fixed diff --git a/changelogs/unreleased/issue-edit-shortcut.yml b/changelogs/unreleased/issue-edit-shortcut.yml new file mode 100644 index 00000000000..2b29b2bc03f --- /dev/null +++ b/changelogs/unreleased/issue-edit-shortcut.yml @@ -0,0 +1,5 @@ +--- +title: Fixed issue edit shortcut not opening edit form +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml b/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml deleted file mode 100644 index 09112fba85e..00000000000 --- a/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only check LFS integrity for first ref in a push to avoid timeout -merge_request: 17098 -author: -type: performance diff --git a/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml b/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml deleted file mode 100644 index 690536a533b..00000000000 --- a/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix single digit value clipping for stacked progress bar -merge_request: 17217 -author: -type: fixed diff --git a/changelogs/unreleased/minimal-fix-for-artifacts-service.yml b/changelogs/unreleased/minimal-fix-for-artifacts-service.yml new file mode 100644 index 00000000000..11f5bc17759 --- /dev/null +++ b/changelogs/unreleased/minimal-fix-for-artifacts-service.yml @@ -0,0 +1,5 @@ +--- +title: Prevent trace artifact migration to incur data loss +merge_request: 17313 +author: +type: fixed diff --git a/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml b/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml new file mode 100644 index 00000000000..a761d610da1 --- /dev/null +++ b/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml @@ -0,0 +1,5 @@ +--- +title: Return a 404 instead of 403 if the repository does not exist on disk +merge_request: 17341 +author: +type: fixed diff --git a/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml b/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml new file mode 100644 index 00000000000..20d05530513 --- /dev/null +++ b/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move BoardNewIssue vue component +merge_request: 16947 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml b/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml new file mode 100644 index 00000000000..d65318d7ba1 --- /dev/null +++ b/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move RecentSearchesDropdownContent vue component +merge_request: 16951 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/sh-guard-read-only-user-updates.yml b/changelogs/unreleased/sh-guard-read-only-user-updates.yml deleted file mode 100644 index b8dbd840ed9..00000000000 --- a/changelogs/unreleased/sh-guard-read-only-user-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't attempt to update user tracked fields if database is in read-only -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/users-autocomplete.yml b/changelogs/unreleased/users-autocomplete.yml deleted file mode 100644 index 2cb078a3a7c..00000000000 --- a/changelogs/unreleased/users-autocomplete.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve performance of searching for and autocompleting of users -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-branch-contains-git-message.yml b/changelogs/unreleased/zj-branch-contains-git-message.yml deleted file mode 100644 index ce034e7ec87..00000000000 --- a/changelogs/unreleased/zj-branch-contains-git-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow branch names to be named the same as the sha it points to -merge_request: -author: -type: fixed diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 0f164e628f9..161fb185c9b 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -10,7 +10,7 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] - chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] + chain.add Gitlab::SidekiqMiddleware::Shutdown chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0' chain.add Gitlab::SidekiqStatus::ServerMiddleware end diff --git a/config/webpack.config.js b/config/webpack.config.js index 1cdd1d83baf..9367dfcc5ec 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -21,63 +21,49 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; -// generate automatic entry points -var autoEntries = {}; -var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') }); - -// filter out entries currently imported dynamically in dispatcher.js -var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString(); -var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g); - -function generateAutoEntries(path, prefix = '.') { - const chunkPath = path.replace(/\/index\.js$/, ''); - if (!dispatcherChunks.includes(`${prefix}/${chunkPath}`)) { +var autoEntriesCount = 0; +var watchAutoEntries = []; + +function generateEntries() { + // generate automatic entry points + var autoEntries = {}; + var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') }); + watchAutoEntries = [ + path.join(ROOT_PATH, 'app/assets/javascripts/pages/'), + ]; + + function generateAutoEntries(path, prefix = '.') { + const chunkPath = path.replace(/\/index\.js$/, ''); const chunkName = chunkPath.replace(/\//g, '.'); autoEntries[chunkName] = `${prefix}/${path}`; } -} -pageEntries.forEach(( path ) => generateAutoEntries(path)); + pageEntries.forEach(( path ) => generateAutoEntries(path)); -// report our auto-generated bundle count -var autoEntriesCount = Object.keys(autoEntries).length; -console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`); + autoEntriesCount = Object.keys(autoEntries).length; -var config = { - // because sqljs requires fs. - node: { - fs: "empty" - }, - context: path.join(ROOT_PATH, 'app/assets/javascripts'), - entry: { + const manualEntries = { balsamiq_viewer: './blob/balsamiq_viewer.js', - blob: './blob_edit/blob_bundle.js', - boards: './boards/boards_bundle.js', + common: './commons/index.js', + common_vue: './vue_shared/vue_resource_interceptor.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', - commit_pipelines: './commit/pipelines/pipelines_bundle.js', - diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', - environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', help: './help/help.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', monitoring: './monitoring/monitoring_bundle.js', - network: './network/network_bundle.js', notebook_viewer: './blob/notebook_viewer.js', pdf_viewer: './blob/pdf_viewer.js', - pipelines: './pipelines/pipelines_bundle.js', profile: './profile/profile_bundle.js', project_import_gl: './projects/project_import_gitlab_project.js', protected_branches: './protected_branches', protected_tags: './protected_tags', registry_list: './registry/index.js', - sidebar: './sidebar/sidebar_bundle.js', snippet: './snippet/snippet_bundle.js', sketch_viewer: './blob/sketch_viewer.js', stl_viewer: './blob/stl_viewer.js', terminal: './terminal/terminal_bundle.js', ui_development_kit: './ui_development_kit.js', - vue_merge_request_widget: './vue_merge_request_widget/index.js', two_factor_auth: './two_factor_auth.js', @@ -90,7 +76,15 @@ var config = { test: './test.js', u2f: ['vendor/u2f'], webpack_runtime: './webpack.js', - }, + }; + + return Object.assign(manualEntries, autoEntries); +} + +var config = { + context: path.join(ROOT_PATH, 'app/assets/javascripts'), + + entry: generateEntries, output: { path: path.join(ROOT_PATH, 'public/assets/webpack'), @@ -243,12 +237,9 @@ var config = { name: 'common_vue', chunks: [ 'boards', - 'commit_pipelines', 'cycle_analytics', 'deploy_keys', - 'diff_notes', 'environments', - 'environments_folder', 'filtered_search', 'groups', 'merge_conflicts', @@ -308,11 +299,15 @@ var config = { 'images': path.join(ROOT_PATH, 'app/assets/images'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.esm.js', + 'spec': path.join(ROOT_PATH, 'spec/javascripts'), } - } -} + }, -config.entry = Object.assign({}, autoEntries, config.entry); + // sqljs requires fs + node: { + fs: 'empty', + }, +}; if (IS_PRODUCTION) { config.devtool = 'source-map'; @@ -349,7 +344,24 @@ if (IS_DEV_SERVER) { }; config.plugins.push( // watch node_modules for changes if we encounter a missing module compile error - new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) + new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')), + + // watch for changes to our automatic entry point modules + { + apply(compiler) { + compiler.plugin('emit', (compilation, callback) => { + compilation.contextDependencies = [ + ...compilation.contextDependencies, + ...watchAutoEntries, + ]; + + // report our auto-generated bundle count + console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`); + + callback(); + }) + }, + }, ); if (DEV_SERVER_LIVERELOAD) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); diff --git a/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb b/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb new file mode 100644 index 00000000000..e55e2e6f888 --- /dev/null +++ b/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb @@ -0,0 +1,14 @@ +class AddTmpPartialNullIndexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', + name: 'tmp_id_partial_null_index') + end + + def down + remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index') + end +end diff --git a/db/post_migrate/20180212101928_schedule_build_stage_migration.rb b/db/post_migrate/20180212101928_schedule_build_stage_migration.rb new file mode 100644 index 00000000000..df15b2cd9d4 --- /dev/null +++ b/db/post_migrate/20180212101928_schedule_build_stage_migration.rb @@ -0,0 +1,29 @@ +class ScheduleBuildStageMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'MigrateBuildStage'.freeze + BATCH_SIZE = 500 + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + self.table_name = 'ci_builds' + end + + def up + disable_statement_timeout + + Build.where('stage_id IS NULL').tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end + end + + def down + # noop + end +end diff --git a/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb b/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb new file mode 100644 index 00000000000..ed7b1fc72f4 --- /dev/null +++ b/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb @@ -0,0 +1,14 @@ +class RemoveTmpPartialNullIndexFromBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index') + end + + def down + add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', + name: 'tmp_id_partial_null_index') + end +end diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index e3b10119090..d9a61aea6ef 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -2,7 +2,9 @@ [Gitaly](https://gitlab.com/gitlab-org/gitaly) (introduced in GitLab 9.0) is a service that provides high-level RPC access to Git -repositories. Gitaly is a mandatory component in GitLab 9.4 and newer. +repositories. Gitaly was optional when it was first introduced in +GitLab, but since GitLab 9.4 it is a mandatory component of the +application. GitLab components that access Git repositories (gitlab-rails, gitlab-shell, gitlab-workhorse) act as clients to Gitaly. End users do @@ -184,14 +186,20 @@ Gitaly logs on your Gitaly server (`sudo gitlab-ctl tail gitaly` or coming in. One sure way to trigger a Gitaly request is to clone a repository from your GitLab server over HTTP. -## Disabling or enabling the Gitaly service +## Disabling or enabling the Gitaly service in a cluster environment If you are running Gitaly [as a remote service](#running-gitaly-on-its-own-server) you may want to disable the local Gitaly service that runs on your Gitlab server by default. -To disable the Gitaly service in your Omnibus installation, add the -following line to `/etc/gitlab/gitlab.rb`: +> 'Disabling Gitaly' only makes sense when you run GitLab in a custom +cluster configuration, where different services run on different +machines. Disabling Gitaly on all machines in the cluster is not a +valid configuration. + +If you are setting up a GitLab cluster where Gitaly does not need to +run on all machines, you can disable the Gitaly service in your +Omnibus installation, add the following line to `/etc/gitlab/gitlab.rb`: ```ruby gitaly['enable'] = false @@ -200,11 +208,13 @@ gitaly['enable'] = false When you run `gitlab-ctl reconfigure` the Gitaly service will be disabled. -To disable the Gitaly service in an installation from source, add the -following to `/etc/default/gitlab`: +To disable the Gitaly service in a GitLab cluster where you installed +GitLab from source, add the following to `/etc/default/gitlab` on the +machine where you want to disable Gitaly. ```shell gitaly_enabled=false ``` -When you run `service gitlab restart` Gitaly will be disabled. +When you run `service gitlab restart` Gitaly will be disabled on this +particular machine. diff --git a/doc/api/projects.md b/doc/api/projects.md index 9e649efea9c..4f4ab906c1a 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1224,7 +1224,7 @@ POST /projects/:id/hooks | `note_events` | boolean | no | Trigger hook on note events | | `job_events` | boolean | no | Trigger hook on job events | | `pipeline_events` | boolean | no | Trigger hook on pipeline events | -| `wiki_events` | boolean | no | Trigger hook on wiki events | +| `wiki_page_events` | boolean | no | Trigger hook on wiki events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | | `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 60ae5e6b9a2..ae13c248171 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -53,7 +53,10 @@ module API put ':id/access_requests/:user_id/approve' do source = find_source(source_type, params[:id]) - member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute + access_requester = source.requesters.find_by!(user_id: params[:user_id]) + member = ::Members::ApproveAccessRequestService + .new(current_user, declared_params) + .execute(access_requester) status :created present member, with: Entities::Member @@ -70,8 +73,7 @@ module API member = source.requesters.find_by!(user_id: params[:user_id]) destroy_conditionally!(member) do - ::Members::DestroyService.new(source, current_user, params) - .execute(:requesters) + ::Members::DestroyService.new(current_user).execute(member) end end end diff --git a/lib/api/members.rb b/lib/api/members.rb index bc1de37284a..8b12986d09e 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -81,12 +81,16 @@ module API source = find_source(source_type, params.delete(:id)) authorize_admin_source!(source_type, source) - member = source.members.find_by!(user_id: params.delete(:user_id)) + member = source.members.find_by!(user_id: params[:user_id]) + updated_member = + ::Members::UpdateService + .new(current_user, declared_params(include_missing: false)) + .execute(member) - if member.update_attributes(declared_params(include_missing: false)) - present member, with: Entities::Member + if updated_member.valid? + present updated_member, with: Entities::Member else - render_validation_error!(member) + render_validation_error!(updated_member) end end @@ -99,7 +103,7 @@ module API member = source.members.find_by!(user_id: params[:user_id]) destroy_conditionally!(member) do - ::Members::DestroyService.new(source, current_user, declared_params).execute + ::Members::DestroyService.new(current_user).execute(member) end end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 51e33e2c686..6c97659166d 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true module API class Services < Grape::API - chat_notification_settings = [ + CHAT_NOTIFICATION_SETTINGS = [ { required: true, name: :webhook, @@ -19,9 +20,9 @@ module API type: String, desc: 'The default chat channel' } - ] + ].freeze - chat_notification_flags = [ + CHAT_NOTIFICATION_FLAGS = [ { required: false, name: :notify_only_broken_pipelines, @@ -34,9 +35,9 @@ module API type: Boolean, desc: 'Send notifications only for the default branch' } - ] + ].freeze - chat_notification_channels = [ + CHAT_NOTIFICATION_CHANNELS = [ { required: false, name: :push_channel, @@ -85,9 +86,9 @@ module API type: String, desc: 'The name of the channel to receive wiki_page_events notifications' } - ] + ].freeze - chat_notification_events = [ + CHAT_NOTIFICATION_EVENTS = [ { required: false, name: :push_events, @@ -136,7 +137,7 @@ module API type: Boolean, desc: 'Enable notifications for wiki_page_events' } - ] + ].freeze services = { 'asana' => [ @@ -627,10 +628,10 @@ module API } ], 'slack' => [ - chat_notification_settings, - chat_notification_flags, - chat_notification_channels, - chat_notification_events + CHAT_NOTIFICATION_SETTINGS, + CHAT_NOTIFICATION_FLAGS, + CHAT_NOTIFICATION_CHANNELS, + CHAT_NOTIFICATION_EVENTS ].flatten, 'microsoft-teams' => [ { @@ -641,10 +642,10 @@ module API } ], 'mattermost' => [ - chat_notification_settings, - chat_notification_flags, - chat_notification_channels, - chat_notification_events + CHAT_NOTIFICATION_SETTINGS, + CHAT_NOTIFICATION_FLAGS, + CHAT_NOTIFICATION_CHANNELS, + CHAT_NOTIFICATION_EVENTS ].flatten, 'teamcity' => [ { @@ -724,7 +725,22 @@ module API ] end - trigger_services = { + SERVICES = services.freeze + SERVICE_CLASSES = service_classes.freeze + + SERVICE_CLASSES.each do |service| + event_names = service.try(:event_names) || next + event_names.each do |event_name| + SERVICES[service.to_param.tr("_", "-")] << { + required: false, + name: event_name.to_sym, + type: String, + desc: ServicesHelper.service_event_description(event_name) + } + end + end + + TRIGGER_SERVICES = { 'mattermost-slash-commands' => [ { name: :token, @@ -756,22 +772,9 @@ module API end end - services.each do |service_slug, settings| + SERVICES.each do |service_slug, settings| desc "Set #{service_slug} service for project" params do - service_classes.each do |service| - event_names = service.try(:event_names) || next - event_names.each do |event_name| - services[service.to_param.tr("_", "-")] << { - required: false, - name: event_name.to_sym, - type: String, - desc: ServicesHelper.service_event_description(event_name) - } - end - end - services.freeze - settings.each do |setting| if setting[:required] requires setting[:name], type: setting[:type], desc: setting[:desc] @@ -794,7 +797,7 @@ module API desc "Delete a service for project" params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service' end delete ":id/services/:service_slug" do service = user_project.find_or_initialize_service(params[:service_slug].underscore) @@ -814,7 +817,7 @@ module API success Entities::ProjectService end params do - requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' + requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service' end get ":id/services/:service_slug" do service = user_project.find_or_initialize_service(params[:service_slug].underscore) @@ -822,7 +825,7 @@ module API end end - trigger_services.each do |service_slug, settings| + TRIGGER_SERVICES.each do |service_slug, settings| helpers do def slash_command_service(project, service_slug, params) project.services.active.where(template: false).find do |service| diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb index d7bde8ceb89..88dd598f1e9 100644 --- a/lib/api/v3/members.rb +++ b/lib/api/v3/members.rb @@ -124,7 +124,7 @@ module API status(200 ) { message: "Access revoked", id: params[:user_id].to_i } else - ::Members::DestroyService.new(source, current_user, declared_params).execute + ::Members::DestroyService.new(current_user).execute(member) present member, with: ::API::Entities::Member end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index e7e6a90b5fd..c9e3f8ce42b 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -174,7 +174,9 @@ module Banzai title = object_link_title(object) klass = reference_class(object_sym) - data = data_attributes_for(link_content || match, parent, object, link: !!link_content) + data = data_attributes_for(link_content || match, parent, object, + link_content: !!link_content, + link_reference: link_reference) url = if matches.names.include?("url") && matches[:url] @@ -194,12 +196,13 @@ module Banzai end end - def data_attributes_for(text, project, object, link: false) + def data_attributes_for(text, project, object, link_content: false, link_reference: false) data_attribute( - original: text, - link: link, - project: project.id, - object_sym => object.id + original: text, + link: link_content, + link_reference: link_reference, + project: project.id, + object_sym => object.id ) end diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb index 827df7c08ae..fd457bebf03 100644 --- a/lib/banzai/redactor.rb +++ b/lib/banzai/redactor.rb @@ -42,16 +42,33 @@ module Banzai next if visible.include?(node) doc_data[:visible_reference_count] -= 1 - # The reference should be replaced by the original link's content, - # which is not always the same as the rendered one. - content = node.attr('data-original') || node.inner_html - node.replace(content) + redacted_content = redacted_node_content(node) + node.replace(redacted_content) end end metadata end + # Return redacted content of given node as either the original link (<a> tag), + # the original content (text), or the inner HTML of the node. + # + def redacted_node_content(node) + original_content = node.attr('data-original') + link_reference = node.attr('data-link-reference') + + # Build the raw <a> tag just with a link as href and content if + # it's originally a link pattern. We shouldn't return a plain text href. + original_link = + if link_reference == 'true' && href = original_content + %(<a href="#{href}">#{href}</a>) + end + + # The reference should be replaced by the original link's content, + # which is not always the same as the rendered one. + original_link || original_content || node.inner_html + end + def redact_cross_project_references(documents) extractor = Banzai::IssuableExtractor.new(project, user) issuables = extractor.extract(documents) diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb new file mode 100644 index 00000000000..8fe4f1a2289 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_build_stage.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +# rubocop:disable Metrics/AbcSize +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class MigrateBuildStage + module Migratable + class Stage < ActiveRecord::Base + self.table_name = 'ci_stages' + end + + class Build < ActiveRecord::Base + self.table_name = 'ci_builds' + + def ensure_stage!(attempts: 2) + find_stage || create_stage! + rescue ActiveRecord::RecordNotUnique + retry if (attempts -= 1) > 0 + raise + end + + def find_stage + Stage.find_by(name: self.stage || 'test', + pipeline_id: self.commit_id, + project_id: self.project_id) + end + + def create_stage! + Stage.create!(name: self.stage || 'test', + pipeline_id: self.commit_id, + project_id: self.project_id) + end + end + end + + def perform(start_id, stop_id) + stages = Migratable::Build.where('stage_id IS NULL') + .where('id BETWEEN ? AND ?', start_id, stop_id) + .map { |build| build.ensure_stage! } + .compact.map(&:id) + + MigrateBuildStageIdReference.new.perform(start_id, stop_id) + MigrateStageStatus.new.perform(stages.min, stages.max) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index e3cbf017e55..ddb9cf433eb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -2206,7 +2206,7 @@ module Gitlab with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do # Apply diff of the `diff_range` to the worktree diff = run_git!(%W(diff --binary #{diff_range})) - run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin| + run_git!(%w(apply --index --whitespace=nowarn), chdir: squash_path, env: env) do |stdin| stdin.binmode stdin.write(diff) end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index ba6058fd3c9..b6ceb542dd1 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -14,14 +14,14 @@ module Gitlab # Uses rugged for raw objects # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320 - def where(repository, sha, path = nil) + def where(repository, sha, path = nil, recursive = false) path = nil if path == '' || path == '/' Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled| if is_enabled - repository.gitaly_commit_client.tree_entries(repository, sha, path) + repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive) else - tree_entries_from_rugged(repository, sha, path) + tree_entries_from_rugged(repository, sha, path, recursive) end end end @@ -57,7 +57,22 @@ module Gitlab end end - def tree_entries_from_rugged(repository, sha, path) + def tree_entries_from_rugged(repository, sha, path, recursive) + current_path_entries = get_tree_entries_from_rugged(repository, sha, path) + ordered_entries = [] + + current_path_entries.each do |entry| + ordered_entries << entry + + if recursive && entry.dir? + ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true)) + end + end + + ordered_entries + end + + def get_tree_entries_from_rugged(repository, sha, path) commit = repository.lookup(sha) root_tree = commit.tree diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index bbdb593d4e2..6400089a22f 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -199,7 +199,7 @@ module Gitlab def check_repository_existence! unless repository.exists? - raise UnauthorizedError, ERROR_MESSAGES[:no_repo] + raise NotFoundError, ERROR_MESSAGES[:no_repo] end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index c5d3e944f7d..9cd76630484 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -125,6 +125,8 @@ module Gitlab kwargs = yield(kwargs) if block_given? stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend + rescue GRPC::Unavailable => ex + handle_grpc_unavailable!(ex) ensure duration = Gitlab::Metrics::System.monotonic_time - start @@ -135,6 +137,27 @@ module Gitlab duration) end + def self.handle_grpc_unavailable!(ex) + status = ex.to_status + raise ex unless status.details == 'Endpoint read failed' + + # There is a bug in grpc 1.8.x that causes a client process to get stuck + # always raising '14:Endpoint read failed'. The only thing that we can + # do to recover is to restart the process. + # + # See https://gitlab.com/gitlab-org/gitaly/issues/1029 + + if Sidekiq.server? + raise Gitlab::SidekiqMiddleware::Shutdown::WantShutdown.new(ex.to_s) + else + # SIGQUIT requests a Unicorn worker to shut down gracefully after the current request. + Process.kill('QUIT', Process.pid) + end + + raise ex + end + private_class_method :handle_grpc_unavailable! + def self.current_transaction_labels Gitlab::Metrics::Transaction.current&.labels || {} end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 269a048cf5d..d60f57717b5 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -105,11 +105,12 @@ module Gitlab entry unless entry.oid.blank? end - def tree_entries(repository, revision, path) + def tree_entries(repository, revision, path, recursive) request = Gitaly::GetTreeEntriesRequest.new( repository: @gitaly_repo, revision: encode_binary(revision), - path: path.present? ? encode_binary(path) : '.' + path: path.present? ? encode_binary(path) : '.', + recursive: recursive ) response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout) diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb index f654508c391..f7a8eae0be4 100644 --- a/lib/gitlab/job_waiter.rb +++ b/lib/gitlab/job_waiter.rb @@ -15,16 +15,22 @@ module Gitlab # push to that array when done. Once the waiter has popped `count` items, it # knows all the jobs are done. class JobWaiter + KEY_PREFIX = "gitlab:job_waiter".freeze + def self.notify(key, jid) Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) } end + def self.key?(key) + key.is_a?(String) && key =~ /\A#{KEY_PREFIX}:\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/ + end + attr_reader :key, :finished attr_accessor :jobs_remaining # jobs_remaining - the number of jobs left to wait for # key - The key of this waiter. - def initialize(jobs_remaining = 0, key = "gitlab:job_waiter:#{SecureRandom.uuid}") + def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}") @key = key @jobs_remaining = jobs_remaining @finished = [] diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb deleted file mode 100644 index b89ae2505c9..00000000000 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ /dev/null @@ -1,67 +0,0 @@ -module Gitlab - module SidekiqMiddleware - class MemoryKiller - # Default the RSS limit to 0, meaning the MemoryKiller is disabled - MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i - # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit - GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i - # Wait 30 seconds for running jobs to finish during graceful shutdown - SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i - - # Create a mutex used to ensure there will be only one thread waiting to - # shut Sidekiq down - MUTEX = Mutex.new - - def call(worker, job, queue) - yield - - current_rss = get_rss - - return unless MAX_RSS > 0 && current_rss > MAX_RSS - - Thread.new do - # Return if another thread is already waiting to shut Sidekiq down - return unless MUTEX.try_lock - - Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\ - " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}" - Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later" - - # Wait `GRACE_TIME` to give the memory intensive job time to finish. - # Then, tell Sidekiq to stop fetching new jobs. - wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs') - - # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish. - # Then, tell Sidekiq to gracefully shut down by giving jobs a few more - # moments to finish, killing and requeuing them if they didn't, and - # then terminating itself. - wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down') - - # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't. - wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die') - end - end - - private - - def get_rss - output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s) - return 0 unless status.zero? - - output.to_i - end - - def wait_and_signal(time, signal, explanation) - Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" - sleep(time) - - Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" - Process.kill(signal, pid) - end - - def pid - Process.pid - end - end - end -end diff --git a/lib/gitlab/sidekiq_middleware/shutdown.rb b/lib/gitlab/sidekiq_middleware/shutdown.rb new file mode 100644 index 00000000000..c2b8d6de66e --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/shutdown.rb @@ -0,0 +1,133 @@ +require 'mutex_m' + +module Gitlab + module SidekiqMiddleware + class Shutdown + extend Mutex_m + + # Default the RSS limit to 0, meaning the MemoryKiller is disabled + MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i + # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit + GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i + # Wait 30 seconds for running jobs to finish during graceful shutdown + SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i + + # This exception can be used to request that the middleware start shutting down Sidekiq + WantShutdown = Class.new(StandardError) + + ShutdownWithoutRaise = Class.new(WantShutdown) + private_constant :ShutdownWithoutRaise + + # For testing only, to avoid race conditions (?) in Rspec mocks. + attr_reader :trace + + # We store the shutdown thread in a class variable to ensure that there + # can be only one shutdown thread in the process. + def self.create_shutdown_thread + mu_synchronize do + return unless @shutdown_thread.nil? + + @shutdown_thread = Thread.new { yield } + end + end + + # For testing only: so we can wait for the shutdown thread to finish. + def self.shutdown_thread + mu_synchronize { @shutdown_thread } + end + + # For testing only: so that we can reset the global state before each test. + def self.clear_shutdown_thread + mu_synchronize { @shutdown_thread = nil } + end + + def initialize + @trace = Queue.new if Rails.env.test? + end + + def call(worker, job, queue) + shutdown_exception = nil + + begin + yield + check_rss! + rescue WantShutdown => ex + shutdown_exception = ex + end + + return unless shutdown_exception + + self.class.create_shutdown_thread do + do_shutdown(worker, job, shutdown_exception) + end + + raise shutdown_exception unless shutdown_exception.is_a?(ShutdownWithoutRaise) + end + + private + + def do_shutdown(worker, job, shutdown_exception) + Sidekiq.logger.warn "Sidekiq worker PID-#{pid} shutting down because of #{shutdown_exception} after job "\ + "#{worker.class} JID-#{job['jid']}" + Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later" + + # Wait `GRACE_TIME` to give the memory intensive job time to finish. + # Then, tell Sidekiq to stop fetching new jobs. + wait_and_signal(GRACE_TIME, 'SIGTSTP', 'stop fetching new jobs') + + # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish. + # Then, tell Sidekiq to gracefully shut down by giving jobs a few more + # moments to finish, killing and requeuing them if they didn't, and + # then terminating itself. + wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down') + + # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't. + wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die') + end + + def check_rss! + return unless MAX_RSS > 0 + + current_rss = get_rss + return unless current_rss > MAX_RSS + + raise ShutdownWithoutRaise.new("current RSS #{current_rss} exceeds maximum RSS #{MAX_RSS}") + end + + def get_rss + output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s) + return 0 unless status.zero? + + output.to_i + end + + def wait_and_signal(time, signal, explanation) + Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" + sleep(time) + + Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})" + kill(signal, pid) + end + + def pid + Process.pid + end + + def sleep(time) + if Rails.env.test? + @trace << [:sleep, time] + else + Kernel.sleep(time) + end + end + + def kill(signal, pid) + if Rails.env.test? + @trace << [:kill, signal, pid] + else + Process.kill(signal, pid) + end + end + end + end +end diff --git a/lib/gitlab/slash_commands/base_command.rb b/lib/gitlab/slash_commands/base_command.rb index cc3c9a50555..466554e398c 100644 --- a/lib/gitlab/slash_commands/base_command.rb +++ b/lib/gitlab/slash_commands/base_command.rb @@ -31,10 +31,11 @@ module Gitlab raise NotImplementedError end - attr_accessor :project, :current_user, :params + attr_accessor :project, :current_user, :params, :chat_name - def initialize(project, user, params = {}) - @project, @current_user, @params = project, user, params.dup + def initialize(project, chat_name, params = {}) + @project, @current_user, @params = project, chat_name.user, params.dup + @chat_name = chat_name end private diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index a78408b0519..85aaa6b0eba 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -13,12 +13,13 @@ module Gitlab if command if command.allowed?(project, current_user) - command.new(project, current_user, params).execute(match) + command.new(project, chat_name, params).execute(match) else Gitlab::SlashCommands::Presenters::Access.new.access_denied end else - Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) + Gitlab::SlashCommands::Help.new(project, chat_name, params) + .execute(available_commands, params[:text]) end end diff --git a/package.json b/package.json index 043af80a3be..cbad55b4c85 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "dev-server": "nodemon -w 'config/webpack.config.js' -w 'app/assets/javascripts/dispatcher.js' -w 'app/assets/javascripts/pages/**/index.js' --exec 'webpack-dev-server --config config/webpack.config.js'", + "dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'", "eslint": "eslint --max-warnings 0 --ext .js,.vue .", "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .", @@ -116,6 +116,6 @@ "karma-webpack": "2.0.7", "nodemon": "^1.15.1", "prettier": "1.9.2", - "webpack-dev-server": "^2.11.1" + "webpack-dev-server": "^2.11.2" } } @@ -130,6 +130,7 @@ module QA autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' autoload :SecretVariables, 'qa/page/project/settings/secret_variables' autoload :Runners, 'qa/page/project/settings/runners' + autoload :MergeRequest, 'qa/page/project/settings/merge_request' end module Issue @@ -145,6 +146,7 @@ module QA module MergeRequest autoload :New, 'qa/page/merge_request/new' + autoload :Show, 'qa/page/merge_request/show' end module Admin diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index bd66b74a164..afaa96b4541 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -22,7 +22,7 @@ module QA factory.fabricate!(*args) - return Factory::Product.populate!(self) + return Factory::Product.populate!(factory) end end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb index d004e642f9b..996b7f14f61 100644 --- a/qa/qa/factory/product.rb +++ b/qa/qa/factory/product.rb @@ -17,8 +17,9 @@ module QA def self.populate!(factory) new.tap do |product| - factory.attributes.each_value do |attribute| - product.instance_exec(&attribute.block).tap do |value| + factory.class.attributes.each_value do |attribute| + product.instance_exec(factory, attribute.block) do |factory, block| + value = block.call(factory) product.define_singleton_method(attribute.name) { value } end end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb index 2f4de4173d4..6e8905cde78 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/factory/repository/push.rb @@ -2,7 +2,7 @@ module QA module Factory module Repository class Push < Factory::Base - attr_writer :file_name, :file_content, :commit_message, :branch_name + attr_writer :file_name, :file_content, :commit_message, :branch_name, :new_branch dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-with-code' @@ -14,6 +14,7 @@ module QA @file_content = '# This is test project' @commit_message = "Add #{@file_name}" @branch_name = 'master' + @new_branch = true end def fabricate! @@ -29,6 +30,7 @@ module QA repository.clone repository.configure_identity('GitLab QA', 'root@gitlab.com') + repository.checkout(@branch_name) unless @new_branch repository.add_file(@file_name, @file_content) repository.commit(@commit_message) repository.push_changes(@branch_name) diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index ce04e904aaf..539fe6b8a70 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -9,11 +9,20 @@ module QA :source_branch, :target_branch + product :project do |factory| + factory.project + end + + product :source_branch do |factory| + factory.source_branch + end + dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-with-merge-request' end dependency Factory::Repository::Push, as: :target do |push, factory| + factory.project.visit! push.project = factory.project push.branch_name = "master:#{factory.target_branch}" end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 4c4ef3ef477..b3150e8f3fa 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -36,6 +36,10 @@ module QA `git clone #{opts} #{@uri.to_s} ./ #{suppress_output}` end + def checkout(branch_name) + `git checkout "#{branch_name}"` + end + def shallow_clone clone('--depth 1') end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb new file mode 100644 index 00000000000..35875487da8 --- /dev/null +++ b/qa/qa/page/merge_request/show.rb @@ -0,0 +1,46 @@ +module QA + module Page + module MergeRequest + class Show < Page::Base + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js' do + element :merge_button + end + + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do + element :merged_status, 'The changes were merged into' + end + + view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do + element :mr_rebase_button + element :fast_forward_nessage, "Fast-forward merge is not possible" + end + + def rebase! + wait(reload: false) do + click_element :mr_rebase_button + + has_text?("The source branch HEAD has recently changed.") + end + end + + def fast_forward_possible? + !has_text?("Fast-forward merge is not possible") + end + + def has_merge_button? + refresh + + has_selector?('.accept-merge-request') + end + + def merge! + wait(reload: false) do + click_element :merge_button + + has_text?("The changes were merged into") + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb new file mode 100644 index 00000000000..b147c91b467 --- /dev/null +++ b/qa/qa/page/project/settings/merge_request.rb @@ -0,0 +1,27 @@ +module QA + module Page + module Project + module Settings + class MergeRequest < QA::Page::Base + include Common + + view 'app/views/projects/_merge_request_fast_forward_settings.html.haml' do + element :radio_button_merge_ff + end + + view 'app/views/projects/edit.html.haml' do + element :merge_request_settings, 'Merge request settings' + element :save_merge_request_changes + end + + def enable_ff_only + expand_section('Merge request settings') do + click_element :radio_button_merge_ff + click_element :save_merge_request_changes + end + end + end + end + end + end +end diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb new file mode 100644 index 00000000000..2a44d42af6f --- /dev/null +++ b/qa/qa/specs/features/merge_request/rebase_spec.rb @@ -0,0 +1,39 @@ +module QA + feature 'merge request rebase', :core do + scenario 'rebases source branch of merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = "only-fast-forward" + end + + Page::Menu::Side.act { go_to_settings } + Page::Project::Settings::MergeRequest.act { enable_ff_only } + + merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.project = project + merge_request.title = 'Needs rebasing' + end + + Factory::Repository::Push.fabricate! do |push| + push.project = project + push.file_name = "other.txt" + push.file_content = "New file added!" + end + + merge_request.visit! + + Page::MergeRequest::Show.perform do |merge_request| + expect(merge_request).to have_content('Needs rebasing') + expect(merge_request).not_to be_fast_forward_possible + expect(merge_request).not_to have_merge_button + + merge_request.rebase! + + expect(merge_request).to have_merge_button + expect(merge_request.fast_forward_possible?).to be_truthy + end + end + end +end diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index c5663049be8..04e04886699 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -7,6 +7,7 @@ describe QA::Factory::Base do before do allow(QA::Factory::Product).to receive(:new).and_return(product) + allow(QA::Factory::Product).to receive(:populate!).and_return(product) end it 'instantiates the factory and calls factory method' do @@ -76,6 +77,7 @@ describe QA::Factory::Base do allow(subject).to receive(:new).and_return(instance) allow(instance).to receive(:mydep).and_return(nil) allow(QA::Factory::Product).to receive(:new) + allow(QA::Factory::Product).to receive(:populate!) end it 'builds all dependencies first' do @@ -89,8 +91,16 @@ describe QA::Factory::Base do describe '.product' do subject do Class.new(described_class) do + def fabricate! + "any" + end + + # Defined only to be stubbed + def self.find_page + end + product :token do - page.do_something_on_page! + find_page.do_something_on_page! 'resulting value' end end @@ -105,16 +115,17 @@ describe QA::Factory::Base do let(:page) { spy('page') } before do - allow(subject).to receive(:new).and_return(factory) + allow(factory).to receive(:class).and_return(subject) allow(QA::Factory::Product).to receive(:new).and_return(product) allow(product).to receive(:page).and_return(page) + allow(subject).to receive(:find_page).and_return(page) end it 'populates product after fabrication' do subject.fabricate! - expect(page).to have_received(:do_something_on_page!) expect(product.token).to eq 'resulting value' + expect(page).to have_received(:do_something_on_page!) end end end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb index fdfb1ec90cc..f245aabbf43 100644 --- a/qa/spec/factory/product_spec.rb +++ b/qa/spec/factory/product_spec.rb @@ -1,9 +1,20 @@ describe QA::Factory::Product do - let(:factory) { spy('factory') } + let(:factory) do + QA::Factory::Base.new + end + + let(:attributes) do + { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) } + end + let(:product) { spy('product') } + before do + allow(QA::Factory::Base).to receive(:attributes).and_return(attributes) + end + describe '.populate!' do - it 'returns a fabrication product' do + it 'returns a fabrication product and define factory attributes as its methods' do expect(described_class).to receive(:new).and_return(product) result = described_class.populate!(factory) do |instance| @@ -11,6 +22,7 @@ describe QA::Factory::Product do end expect(result).to be product + expect(result.test).to eq('returned') end end diff --git a/spec/features/admin/services/admin_activates_prometheus_spec.rb b/spec/features/admin/services/admin_activates_prometheus_spec.rb new file mode 100644 index 00000000000..904fe5b406b --- /dev/null +++ b/spec/features/admin/services/admin_activates_prometheus_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'Admin activates Prometheus' do + let(:admin) { create(:user, :admin) } + + before do + sign_in(admin) + + visit(admin_application_settings_services_path) + + click_link('Prometheus') + end + + it 'activates service' do + check('Active') + fill_in('API URL', with: 'http://prometheus.example.com') + click_button('Save') + + expect(page).to have_content('Application settings saved successfully') + end +end diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index 243e8536168..04217fec06c 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Groups Merge Requests Empty States' do +feature 'Group empty states' do let(:group) { create(:group) } let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user } @@ -8,62 +8,100 @@ feature 'Groups Merge Requests Empty States' do sign_in(user) end - context 'group has a project' do - let(:project) { create(:project, namespace: group) } + [:issue, :merge_request].each do |issuable| + issuable_name = issuable.to_s.humanize.downcase + project_relation = issuable == :issue ? :project : :source_project - before do - project.add_master(user) - end + context "for #{issuable_name}s" do + let(:path) { public_send(:"#{issuable}s_group_path", group) } - context 'the project has a merge request' do - before do - create(:merge_request, source_project: project) + context 'group has a project' do + let(:project) { create(:project, namespace: group) } - visit merge_requests_group_path(group) - end + before do + project.add_master(user) + end - it 'should not display an empty state' do - expect(page).not_to have_selector('.empty-state') - end - end + context "the project has #{issuable_name}s" do + before do + create(issuable, project_relation => project) - context 'the project has no merge requests', :js do - before do - visit merge_requests_group_path(group) - end + visit path + end - it 'should display an empty state' do - expect(page).to have_selector('.empty-state') - end + it 'does not display an empty state' do + expect(page).not_to have_selector('.empty-state') + end + end + + context "the project has no #{issuable_name}s", :js do + before do + visit path + end + + it 'displays an empty state' do + expect(page).to have_selector('.empty-state') + end + + it "shows a new #{issuable_name} button" do + within '.empty-state' do + expect(page).to have_content("create #{issuable_name}") + end + end + + it "the new #{issuable_name} button opens a project dropdown" do + within '.empty-state' do + find('.new-project-item-select-button').click + end - it 'should show a new merge request button' do - within '.empty-state' do - expect(page).to have_content('create merge request') + expect(page).to have_selector('.ajax-project-dropdown') + end end end - it 'the new merge request button opens a project dropdown' do - within '.empty-state' do - find('.new-project-item-select-button').click - end + context 'group without a project' do + context 'group has a subgroup', :nested_groups do + let(:subgroup) { create(:group, parent: group) } + let(:subgroup_project) { create(:project, namespace: subgroup) } - expect(page).to have_selector('.ajax-project-dropdown') - end - end - end + context "the project has #{issuable_name}s" do + before do + create(issuable, project_relation => subgroup_project) - context 'group without a project' do - before do - visit merge_requests_group_path(group) - end + visit path + end - it 'should display an empty state' do - expect(page).to have_selector('.empty-state') - end + it 'does not display an empty state' do + expect(page).not_to have_selector('.empty-state') + end + end - it 'should not show a new merge request button' do - within '.empty-state' do - expect(page).not_to have_link('create merge request') + context "the project has no #{issuable_name}s" do + before do + visit path + end + + it 'displays an empty state' do + expect(page).to have_selector('.empty-state') + end + end + end + + context 'group has no subgroups' do + before do + visit path + end + + it 'displays an empty state' do + expect(page).to have_selector('.empty-state') + end + + it "shows a new #{issuable_name} button" do + within '.empty-state' do + expect(page).not_to have_link("create #{issuable_name}") + end + end + end end end end diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members_spec.rb index 21f7b4999ad..21f7b4999ad 100644 --- a/spec/features/groups/members/manage_members.rb +++ b/spec/features/groups/members/manage_members_spec.rb diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb index f355cec3ba9..41b9ada988a 100644 --- a/spec/features/issues/filtered_search/recent_searches_spec.rb +++ b/spec/features/issues/filtered_search/recent_searches_spec.rb @@ -39,8 +39,8 @@ describe 'Recent searches', :js do items = all('.filtered-search-history-dropdown-item', visible: false, count: 2) - expect(items[0].text).to eq('label:~qux garply') - expect(items[1].text).to eq('label:~foo bar') + expect(items[0].text).to eq('label: ~qux garply') + expect(items[1].text).to eq('label: ~foo bar') end it 'saved recent searches are restored last on the list' do diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index faf14be4818..ef6b8edd0ad 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -189,6 +189,18 @@ describe 'New/edit issue', :js do expect(find('.js-label-select')).to have_content('Labels') end + it 'clears label search input field when a label is selected' do + click_button 'Labels' + + page.within '.dropdown-menu-labels' do + search_field = find('input[type="search"]') + + search_field.set(label2.title) + click_link label2.title + expect(search_field.value).to eq '' + end + end + it 'correctly updates the selected user when changing assignee' do click_button 'Unassigned' @@ -271,6 +283,18 @@ describe 'New/edit issue', :js do end end + context 'inline edit' do + before do + visit project_issue_path(project, issue) + end + + it 'opens inline edit form with shortcut' do + find('body').send_keys('e') + + expect(page).to have_selector('.detail-page-description form') + end + end + describe 'sub-group project' do let(:group) { create(:group) } let(:nested_group_1) { create(:group, parent: group) } diff --git a/spec/features/projects/services/user_activates_prometheus_spec.rb b/spec/features/projects/services/user_activates_prometheus_spec.rb new file mode 100644 index 00000000000..33f884eb148 --- /dev/null +++ b/spec/features/projects/services/user_activates_prometheus_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'User activates Prometheus' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_settings_integrations_path(project)) + + click_link('Prometheus') + end + + it 'activates service' do + check('Active') + fill_in('API URL', with: 'http://prometheus.example.com') + click_button('Save changes') + + expect(page).to have_content('Prometheus activated.') + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 645d12da09f..cfe979a8647 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -146,8 +146,8 @@ feature 'Project' do end describe 'removal', :js do - let(:user) { create(:user, username: 'test', name: 'test') } - let(:project) { create(:project, namespace: user.namespace, name: 'project1') } + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } before do sign_in(user) @@ -156,8 +156,8 @@ feature 'Project' do end it 'removes a project' do - expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1) - expect(page).to have_content "Project 'test / project1' is in the process of being deleted." + expect { remove_with_confirm('Remove project', project.path) }.to change { Project.count }.by(-1) + expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted." expect(Project.all.count).to be_zero expect(project.issues).to be_empty expect(project.merge_requests).to be_empty diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 06031aee217..dc76efea35b 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -5,6 +5,8 @@ describe LabelsFinder do let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } + let(:private_group_1) { create(:group, :private) } + let(:private_subgroup_1) { create(:group, :private, parent: private_group_1) } let(:project_1) { create(:project, namespace: group_1) } let(:project_2) { create(:project, namespace: group_2) } @@ -20,6 +22,8 @@ describe LabelsFinder do let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1 (group)') } let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') } let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') } + let!(:private_group_label_1) { create(:group_label, group: private_group_1, title: 'Private Group Label 1') } + let!(:private_subgroup_label_1) { create(:group_label, group: private_subgroup_1, title: 'Private Sub Group Label 1') } let(:user) { create(:user) } @@ -66,6 +70,25 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_1] end end + + context 'when including labels from group ancestors', :nested_groups do + it 'returns labels from group and its ancestors' do + private_group_1.add_developer(user) + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_subgroup_1.id, only_group_labels: true, include_ancestor_groups: true) + + expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1] + end + + it 'ignores labels from groups which user can not read' do + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_subgroup_1.id, only_group_labels: true, include_ancestor_groups: true) + + expect(finder.execute).to eq [private_subgroup_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index a030796c54e..1fa194fe1b8 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -86,7 +86,7 @@ describe BlobHelper do it 'verifies blob is text' do expect(helper).not_to receive(:blob_text_viewable?) - button = edit_blob_link(project, 'refs/heads/master', 'README.md') + button = edit_blob_button(project, 'refs/heads/master', 'README.md') expect(button).to start_with('<button') end @@ -96,17 +96,17 @@ describe BlobHelper do expect(project.repository).not_to receive(:blob_at) - edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob) + edit_blob_button(project, 'refs/heads/master', 'README.md', blob: blob) end it 'returns a link with the proper route' do - link = edit_blob_link(project, 'master', 'README.md') + link = edit_blob_button(project, 'master', 'README.md') expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md") end it 'returns a link with the passed link_opts on the expected route' do - link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 }) + link = edit_blob_button(project, 'master', 'README.md', link_opts: { mr_id: 10 }) expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md?mr_id=10") end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7fa665aecdc..2fecd1a3d27 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -173,23 +173,23 @@ describe IssuablesHelper do @project = issue.project expected_data = { - 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}", - 'updateEndpoint' => "/#{@project.full_path}/issues/#{issue.iid}.json", - 'canUpdate' => true, - 'canDestroy' => true, - 'issuableRef' => "##{issue.iid}", - 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown", - 'markdownDocsPath' => '/help/user/markdown', - 'issuableTemplates' => [], - 'projectPath' => @project.path, - 'projectNamespace' => @project.namespace.path, - 'initialTitleHtml' => issue.title, - 'initialTitleText' => issue.title, - 'initialDescriptionHtml' => '<p dir="auto">issue text</p>', - 'initialDescriptionText' => 'issue text', - 'initialTaskStatus' => '0 of 0 tasks completed' + endpoint: "/#{@project.full_path}/issues/#{issue.iid}", + updateEndpoint: "/#{@project.full_path}/issues/#{issue.iid}.json", + canUpdate: true, + canDestroy: true, + issuableRef: "##{issue.iid}", + markdownPreviewPath: "/#{@project.full_path}/preview_markdown", + markdownDocsPath: '/help/user/markdown', + issuableTemplates: [], + projectPath: @project.path, + projectNamespace: @project.namespace.path, + initialTitleHtml: issue.title, + initialTitleText: issue.title, + initialDescriptionHtml: '<p dir="auto">issue text</p>', + initialDescriptionText: 'issue text', + initialTaskStatus: '0 of 0 tasks completed' } - expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data) + expect(helper.issuable_initial_data(issue)).to eq(expected_data) end end diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index e204985f039..d5fbfdeaa91 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import boardNewIssue from '~/boards/components/board_new_issue'; +import boardNewIssue from '~/boards/components/board_new_issue.vue'; import '~/boards/models/list'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index cac785fd3c6..270f925e699 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -1,5 +1,5 @@ import VariableList from '~/ci_variable_list/ci_variable_list'; -import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; const HIDE_CLASS = 'hide'; diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index a9e244e523d..a5cd247b689 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -7,7 +7,7 @@ import { REQUEST_SUCCESS, REQUEST_FAILURE, } from '~/clusters/constants'; -import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('Clusters', () => { let cluster; diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js index e671c18e1a5..2c4707bb856 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/javascripts/clusters/components/application_row_spec.js @@ -12,7 +12,7 @@ import { REQUEST_FAILURE, } from '~/clusters/constants'; import applicationRow from '~/clusters/components/application_row.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; describe('Application Row', () => { diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 1a8affad4e3..15832c38f25 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import applications from '~/clusters/components/applications.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Applications', () => { let vm; diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js index 90f290e845e..421fe62a1e7 100644 --- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js +++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Commit pipeline status component', () => { let vm; diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js index 64a76a6ee5f..2815bdba0c2 100644 --- a/spec/javascripts/cycle_analytics/banner_spec.js +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import banner from '~/cycle_analytics/components/banner.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Cycle analytics banner', () => { let vm; diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js index ad0fc38a856..691e03cb8a6 100644 --- a/spec/javascripts/cycle_analytics/total_time_component_spec.js +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import component from '~/cycle_analytics/components/total_time_component.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Total time component', () => { let vm; diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js index 82de35933f5..10a19af4175 100644 --- a/spec/javascripts/environments/emtpy_state_spec.js +++ b/spec/javascripts/environments/emtpy_state_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import emptyState from '~/environments/components/empty_state.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('environments empty state', () => { let vm; diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 9bd42863759..0e5e50a59a5 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import environmentTableComp from '~/environments/components/environments_table.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Environment table', () => { let Component; diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index a41a4e5a3f7..5bb37304372 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,9 +1,9 @@ import _ from 'underscore'; import Vue from 'vue'; import environmentsComponent from '~/environments/components/environments_app.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import { environment, folder } from './mock_data'; -import { headersInterceptor } from '../helpers/vue_resource_helper'; -import mountComponent from '../helpers/vue_mount_component_helper'; describe('Environment', () => { const mockData = { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index a085074d312..906a1116974 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,9 +1,9 @@ import _ from 'underscore'; import Vue from 'vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; +import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { environmentsList } from '../mock_data'; -import { headersInterceptor } from '../../helpers/vue_resource_helper'; -import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Environments Folder View', () => { let Component; diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js index 34ffc7b1016..1b1f28f3ddb 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -8,7 +8,7 @@ import { mouseenter, inserted, } from '~/feature_highlight/feature_highlight_helper'; -import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('feature highlight helper', () => { describe('getSelector', () => { diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js index 4a516c517ef..59bd2650081 100644 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import eventHub from '~/filtered_search/event_hub'; -import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; - +import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; const createComponent = (propsData) => { diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index 618d0022e4f..e3c942597a3 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -3,10 +3,9 @@ import * as urlUtils from '~/lib/utils/url_utility'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import eventHub from '~/groups/event_hub'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockParentGroupItem, mockChildren } from '../mock_data'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { const Component = Vue.extend(groupItemComponent); diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js index 90e818c1545..793c4909d89 100644 --- a/spec/javascripts/groups/components/groups_spec.js +++ b/spec/javascripts/groups/components/groups_spec.js @@ -4,10 +4,9 @@ import groupsComponent from '~/groups/components/groups.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; import eventHub from '~/groups/event_hub'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockGroups, mockPageInfo } from '../mock_data'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - const createComponent = (searchEmpty = false) => { const Component = Vue.extend(groupsComponent); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index acccbe639c4..15fd37ebcd2 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -2,10 +2,9 @@ import Vue from 'vue'; import itemActionsComponent from '~/groups/components/item_actions.vue'; import eventHub from '~/groups/event_hub'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockParentGroupItem, mockChildren } from '../mock_data'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => { const Component = Vue.extend(itemActionsComponent); diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js index 8faad455825..36f838a104f 100644 --- a/spec/javascripts/groups/components/item_caret_spec.js +++ b/spec/javascripts/groups/components/item_caret_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import itemCaretComponent from '~/groups/components/item_caret.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const createComponent = (isGroupOpen = false) => { const Component = Vue.extend(itemCaretComponent); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js index 55a7a713ca6..ee7ee18259e 100644 --- a/spec/javascripts/groups/components/item_stats_spec.js +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import itemStatsComponent from '~/groups/components/item_stats.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockParentGroupItem, ITEM_TYPE, @@ -9,8 +10,6 @@ import { PROJECT_VISIBILITY_TYPE, } from '../mock_data'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - const createComponent = (item = mockParentGroupItem) => { const Component = Vue.extend(itemStatsComponent); diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js index e990870aaa6..5e35ae4d36c 100644 --- a/spec/javascripts/groups/components/item_stats_value_spec.js +++ b/spec/javascripts/groups/components/item_stats_value_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import itemStatsValueComponent from '~/groups/components/item_stats_value.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => { const Component = Vue.extend(itemStatsValueComponent); diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js index 495cc97b475..24380689b29 100644 --- a/spec/javascripts/groups/components/item_type_icon_spec.js +++ b/spec/javascripts/groups/components/item_type_icon_spec.js @@ -1,10 +1,9 @@ import Vue from 'vue'; import itemTypeIconComponent from '~/groups/components/item_type_icon.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { ITEM_TYPE } from '../mock_data'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => { const Component = Vue.extend(itemTypeIconComponent); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 1c9f48028f2..584db6c6632 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -6,8 +6,8 @@ import '~/render_gfm'; import * as urlUtils from '~/lib/utils/url_utility'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; +import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import issueShowData from '../mock_data'; -import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 0da25bdca9c..ff7f99eec14 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import descriptionComponent from '~/issue_show/components/description.vue'; import * as taskList from '~/task_list'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Description component', () => { let vm; diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index a9df0418d5d..0961605ce5c 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import headerComponent from '~/jobs/components/header.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Job details header', () => { let HeaderComponent; diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js index 440a6585d57..a6fe9fb65e9 100644 --- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; import * as urlUtility from '~/lib/utils/url_utility'; -import mountComponent from '../../../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('stop_jobs_modal.vue', () => { const props = { diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js index 3cd33a3e900..6074e06fcec 100644 --- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js +++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js @@ -5,7 +5,7 @@ import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_mi import eventHub from '~/pages/milestones/shared/event_hub'; import * as urlUtility from '~/lib/utils/url_utility'; -import mountComponent from '../../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('delete_milestone_modal.vue', () => { const Component = Vue.extend(deleteMilestoneModal); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index c3dc7b53d0f..ce181a1e515 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import jobComponent from '~/pipelines/components/graph/job_component.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('pipeline graph job component', () => { let JobComponent; diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index a99ebc4e51a..54d5bfd51e6 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import Vue from 'vue'; import pipelinesComp from '~/pipelines/components/pipelines.vue'; import Store from '~/pipelines/stores/pipelines_store'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js index 588b61196a5..a0939ff5c20 100644 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('DeleteAccountModal component', () => { const actionUrl = `${gl.TEST_HOST}/delete/user`; diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js index 42f0f6fc1af..2054fef790b 100644 --- a/spec/javascripts/projects_dropdown/components/app_spec.js +++ b/spec/javascripts/projects_dropdown/components/app_spec.js @@ -6,7 +6,7 @@ import eventHub from '~/projects_dropdown/event_hub'; import ProjectsStore from '~/projects_dropdown/store/projects_store'; import ProjectsService from '~/projects_dropdown/service/projects_service'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { currentSession, mockProject, mockRawProject } from '../mock_data'; const createComponent = () => { diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js index fcd0f6a3630..2bafb4e81ca 100644 --- a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js +++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockFrequents } from '../mock_data'; const createComponent = () => { diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js index edef150dd1e..c193258474e 100644 --- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js +++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockProject } from '../mock_data'; const createComponent = () => { diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js index 67f8a8946c2..c4b86d77034 100644 --- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js +++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mockProject } from '../mock_data'; const createComponent = () => { diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js index 24d8a00b254..601264258c2 100644 --- a/spec/javascripts/projects_dropdown/components/search_spec.js +++ b/spec/javascripts/projects_dropdown/components/search_spec.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import searchComponent from '~/projects_dropdown/components/search.vue'; import eventHub from '~/projects_dropdown/event_hub'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const createComponent = () => { const Component = Vue.extend(searchComponent); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 6a8a85e3dfb..cf1d0625397 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import Vue from 'vue'; import registry from '~/registry/components/app.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { reposServerResponse } from '../mock_data'; describe('Registry List', () => { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js index debde1bb357..b509cedbe80 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list collapsed', () => { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js index 4b20fdf70d6..6f1a1d874d3 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import listItem from '~/ide/components/commit_sidebar/list_item.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list item', () => { diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js index cb5240ad118..aeb9de9ace4 100644 --- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js index 3f8f37d2343..935da259a99 100644 --- a/spec/javascripts/repo/components/ide_context_bar_spec.js +++ b/spec/javascripts/repo/components/ide_context_bar_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ideContextBar from '~/ide/components/ide_context_bar.vue'; -import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; describe('Multi-file editor right context bar', () => { let vm; diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js index 30e45169205..79c3c8128e8 100644 --- a/spec/javascripts/repo/components/ide_side_bar_spec.js +++ b/spec/javascripts/repo/components/ide_side_bar_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ideSidebar from '~/ide/components/ide_side_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; -import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; describe('IdeSidebar', () => { let vm; diff --git a/spec/javascripts/repo/components/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js index acfd63eb8de..18135177b5e 100644 --- a/spec/javascripts/repo/components/ide_spec.js +++ b/spec/javascripts/repo/components/ide_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; -import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../helpers'; describe('ide component', () => { diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js index cd1d073ec18..82597fc75e8 100644 --- a/spec/javascripts/repo/components/new_branch_form_spec.js +++ b/spec/javascripts/repo/components/new_branch_form_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import newBranchForm from '~/ide/components/new_branch_form.vue'; -import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../helpers'; describe('Multi-file editor new branch form', () => { diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index 6efbbf6d75e..4a8e4445e2f 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import newDropdown from '~/ide/components/new_dropdown/index.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../../helpers'; describe('new dropdown component', () => { diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js index 8bbc3100357..d6a1fdd115c 100644 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import service from '~/ide/services'; import modal from '~/ide/components/new_dropdown/modal.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../../helpers'; describe('new file modal component', () => { diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js index 667112ab21a..ee8aab3a252 100644 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import upload from '~/ide/components/new_dropdown/upload.vue'; import store from '~/ide/stores'; import service from '~/ide/services'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../../helpers'; describe('new dropdown upload', () => { diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 93e94b4f24c..934ada9dec2 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -3,7 +3,7 @@ import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/ide/stores'; import service from '~/ide/services'; import repoCommitSection from '~/ide/components/repo_commit_section.vue'; -import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; +import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import { file, resetStore } from '../helpers'; describe('RepoCommitSection', () => { diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js index b0ea8ae0206..deeea669de8 100644 --- a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js +++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('EditFormButtons', () => { let vm1; diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js index 30cc549c7c0..2a3b60c399c 100644 --- a/spec/javascripts/sidebar/participants_spec.js +++ b/spec/javascripts/sidebar/participants_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import participants from '~/sidebar/components/participants/participants.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const PARTICIPANT = { id: 1, diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 6bb6d639f24..2fbb7268e0b 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -4,8 +4,8 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarStore from '~/sidebar/stores/sidebar_store'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import Mock from './mock_data'; -import mountComponent from '../helpers/vue_mount_component_helper'; describe('sidebar assignees', () => { let vm; diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js index a6113cb0bae..56a2543660b 100644 --- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js +++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js @@ -4,7 +4,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import eventHub from '~/sidebar/event_hub'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import Mock from './mock_data'; describe('Sidebar Subscriptions', function () { diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 79db05f04ed..aee8f0acbb9 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Subscriptions', function () { let vm; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 323b8a9572d..94fcc6c7f2b 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -113,7 +113,9 @@ if (process.env.BABEL_ENV === 'coverage') { // exempt these files from the coverage report const troubleMakers = [ './blob_edit/blob_bundle.js', - './boards/boards_bundle.js', + './boards/components/modal/empty_state.js', + './boards/components/modal/footer.js', + './boards/components/modal/header.js', './cycle_analytics/cycle_analytics_bundle.js', './cycle_analytics/components/stage_plan_component.js', './cycle_analytics/components/stage_staging_component.js', diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js index f14d5f6f76c..db27aa144d6 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetAuthor', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js index 8c55622b15e..6784b498c29 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetAuthorTime', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 13e5595bbfc..235c33fac0d 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetHeader', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js index cc43639f576..367c499daaf 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMergeHelp', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index d7af956c9c1..431cb7f3913 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from '../mock_data'; describe('MRWidgetPipeline', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js index 66ecaa316c8..b453d180a40 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Merge request widget rebase component', () => { let Component; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js index 637bf483deb..5de6ac4079d 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetRelatedLinks', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js index c39fcda0071..0b25500caf4 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MR widget status icon component', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js index f98ebdb38e6..e818f87b4c8 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetArchived', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index 95c94e95e3a..d069dc3fcc6 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetAutoMergeFailed', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 658cadddb81..658612aad3c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetChecking', () => { let Component; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index 51a34739ee9..0e3c134d3ac 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetClosed', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index a7d69fdcdb9..5323523abc0 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetConflicts', () => { let Component; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index a57b9811e08..dd1d62cd4ed 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetFailedToMerge', () => { let Component; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js index df56c4e2c5c..dd907ad9015 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMergeWhenPipelineSucceeds', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 43a989393ba..c2c92d8ac56 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMerged', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js index 0b2ed2d4086..d2d219e4bdb 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMerging', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index 3d7f4abd420..34f76b39b28 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetMissingBranch', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js index c89e863d904..9f8b96c118b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetNotAllowed', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index edab26286bc..baacbc03fb1 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetPipelineBlocked', () => { let vm; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 45035effe81..18ba34b55a5 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -3,8 +3,8 @@ import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; import eventHub from '~/vue_merge_request_widget/event_hub'; import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; -import mountComponent from '../helpers/vue_mount_component_helper'; const returnPromise = data => new Promise((resolve) => { resolve({ diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js index 8762ce9903b..668742ebaee 100644 --- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js +++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import ciBadge from '~/vue_shared/components/ci_badge_link.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('CI Badge Link Component', () => { let CIBadge; diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js index 08e4e1f8337..d0fc10d69ea 100644 --- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js +++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('clipboard button', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js index a33ab689dd1..f19589d3b75 100644 --- a/spec/javascripts/vue_shared/components/expand_button_spec.js +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import expandButton from '~/vue_shared/components/expand_button.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('expand button', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js index d99b17bdc79..f7581251bf0 100644 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import fileIcon from '~/vue_shared/components/file_icon.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('File Icon component', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js index d6148cb785b..2805d9a7003 100644 --- a/spec/javascripts/vue_shared/components/gl_modal_spec.js +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import GlModal from '~/vue_shared/components/gl_modal.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const modalComponent = Vue.extend(GlModal); diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index b378a0bd896..65499a2d730 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import headerCi from '~/vue_shared/components/header_ci_component.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Header CI Component', () => { let HeaderCi; diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js index a22b6bd3a67..68d57ebc8f0 100644 --- a/spec/javascripts/vue_shared/components/icon_spec.js +++ b/spec/javascripts/vue_shared/components/icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Icon from '~/vue_shared/components/icon.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Sprite Icon Component', function () { describe('Initialization', function () { diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js index 24484796bf1..e6ed77dbb52 100644 --- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js +++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import issueWarning from '~/vue_shared/components/issue/issue_warning.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const IssueWarning = Vue.extend(issueWarning); diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js index 49bf8ee6f7c..51c19cd4080 100644 --- a/spec/javascripts/vue_shared/components/loading_button_spec.js +++ b/spec/javascripts/vue_shared/components/loading_button_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import loadingButton from '~/vue_shared/components/loading_button.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const LABEL = 'Hello'; diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index a5f9c75be4e..8412df74f98 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const modalComponent = Vue.extend(modal); diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js index 78e7d747b92..09fda95d7d3 100644 --- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js +++ b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import navigationTabs from '~/vue_shared/components/navigation_tabs.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('navigation tabs component', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js index 7b8e6c330c2..262571efcb8 100644 --- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('placeholder system note component', () => { let PlaceholderSystemNote; diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js index 70ce3dffaba..8efcb54659d 100644 --- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import panelResizer from '~/vue_shared/components/panel_resizer.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Panel Resizer component', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js index 47af9534737..b349e2a2a81 100644 --- a/spec/javascripts/vue_shared/components/pikaday_spec.js +++ b/spec/javascripts/vue_shared/components/pikaday_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import datePicker from '~/vue_shared/components/pikaday.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('datePicker', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js index cce53193870..8c296af6652 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('collapsedCalendarIcon', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 2de108da2ac..9d60f9c758f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('collapsedGroupedDatePicker', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js index 926e11b4d30..8840a5a9dbf 100644 --- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('sidebarDatePicker', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js index 752a9e89d50..c911a129173 100644 --- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('toggleSidebar', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js index a5db0b2c59e..bbd50863069 100644 --- a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js +++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Skeleton loading container', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js index 6940b04573e..de3bf667fb3 100644 --- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const createComponent = (config) => { const Component = Vue.extend(stackedProgressBarComponent); diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js index 859995d33fa..71952cc39e0 100644 --- a/spec/javascripts/vue_shared/components/toggle_button_spec.js +++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Toggle Button', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js index aa93134f2dd..446f025c127 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { placeholderImage } from '~/lazy_loader'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; const DEFAULT_PROPS = { size: 99, diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 1fa89137972..441f3725985 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -40,6 +40,16 @@ describe Banzai::Redactor do expect(doc.to_html).to eq(original_content) end end + + it 'returns <a> tag with original href if it is originally a link reference' do + href = 'http://localhost:3000' + doc = Nokogiri::HTML + .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>") + + redactor.redact([doc]) + + expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>') + end end context 'when project is in pending delete' do diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb new file mode 100644 index 00000000000..e112e9e9e3d --- /dev/null +++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 20180212101928 do + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:stages) { table(:ci_stages) } + let(:jobs) { table(:ci_builds) } + + STATUSES = { created: 0, pending: 1, running: 2, success: 3, + failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + + before do + projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce') + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + + jobs.create!(id: 1, commit_id: 1, project_id: 123, + stage_idx: 2, stage: 'build', status: :success) + jobs.create!(id: 2, commit_id: 1, project_id: 123, + stage_idx: 2, stage: 'build', status: :success) + jobs.create!(id: 3, commit_id: 1, project_id: 123, + stage_idx: 1, stage: 'test', status: :failed) + jobs.create!(id: 4, commit_id: 1, project_id: 123, + stage_idx: 1, stage: 'test', status: :success) + jobs.create!(id: 5, commit_id: 1, project_id: 123, + stage_idx: 3, stage: 'deploy', status: :pending) + jobs.create!(id: 6, commit_id: 1, project_id: 123, + stage_idx: 3, stage: nil, status: :pending) + end + + it 'correctly migrates builds stages' do + expect(stages.count).to be_zero + + described_class.new.perform(1, 6) + + expect(stages.count).to eq 3 + expect(stages.all.pluck(:name)).to match_array %w[test build deploy] + expect(jobs.where(stage_id: nil)).to be_one + expect(jobs.find_by(stage_id: nil).id).to eq 6 + expect(stages.all.pluck(:status)).to match_array [STATUSES[:success], + STATUSES[:failed], + STATUSES[:pending]] + end + + it 'recovers from unique constraint violation only twice' do + allow(described_class::Migratable::Stage) + .to receive(:find_by).and_return(nil) + + expect(described_class::Migratable::Stage) + .to receive(:find_by).exactly(3).times + + expect { described_class.new.perform(1, 6) } + .to raise_error ActiveRecord::RecordNotUnique + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index d601a383a98..13358995383 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2283,6 +2283,20 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(subject).to match(/\h{40}/) end end + + context 'with trailing whitespace in an invalid patch', :skip_gitaly_mock do + let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" } + + it 'does not include whitespace warnings in the error' do + allow(repository).to receive(:run_git!).and_call_original + allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) + + expect { subject }.to raise_error do |error| + expect(error).to be_a(described_class::GitError) + expect(error.message).not_to include('trailing whitespace') + end + end + end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 19d3f55501e..6f07e423c1b 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -534,6 +534,19 @@ describe Gitlab::GitAccess do expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') end + context 'when the project repository does not exist' do + it 'returns not found' do + project.add_guest(user) + repo = project.repository + FileUtils.rm_rf(repo.path) + + # Sanity check for rm_rf + expect(repo.exists?).to eq(false) + + expect { pull_access_check }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.') + end + end + describe 'without access to project' do context 'pull code' do it { expect { pull_access_check }.to raise_not_found } diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 215f1ecc9c5..730ede99fc9 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -57,7 +57,7 @@ describe Gitlab::GitAccessWiki do # Sanity check for rm_rf expect(wiki_repo.exists?).to eq(false) - expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.') + expect { subject }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.') end end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 001c4d3e10a..9be3fa633a7 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -113,7 +113,7 @@ describe Gitlab::GitalyClient::CommitService do .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) .and_return([]) - client.tree_entries(repository, revision, path) + client.tree_entries(repository, revision, path, false) end context 'with UTF-8 params strings' do @@ -126,7 +126,7 @@ describe Gitlab::GitalyClient::CommitService do .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) .and_return([]) - client.tree_entries(repository, revision, path) + client.tree_entries(repository, revision, path, false) end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb deleted file mode 100644 index 8fdbbacd04d..00000000000 --- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe Gitlab::SidekiqMiddleware::MemoryKiller do - subject { described_class.new } - let(:pid) { 999 } - - let(:worker) { double(:worker, class: 'TestWorker') } - let(:job) { { 'jid' => 123 } } - let(:queue) { 'test_queue' } - - def run - thread = subject.call(worker, job, queue) { nil } - thread&.join - end - - before do - allow(subject).to receive(:get_rss).and_return(10.kilobytes) - allow(subject).to receive(:pid).and_return(pid) - end - - context 'when MAX_RSS is set to 0' do - before do - stub_const("#{described_class}::MAX_RSS", 0) - end - - it 'does nothing' do - expect(subject).not_to receive(:sleep) - - run - end - end - - context 'when MAX_RSS is exceeded' do - before do - stub_const("#{described_class}::MAX_RSS", 5.kilobytes) - end - - it 'sends the STP, TERM and KILL signals at expected times' do - expect(subject).to receive(:sleep).with(15 * 60).ordered - expect(Process).to receive(:kill).with('SIGSTP', pid).ordered - - expect(subject).to receive(:sleep).with(30).ordered - expect(Process).to receive(:kill).with('SIGTERM', pid).ordered - - expect(subject).to receive(:sleep).with(10).ordered - expect(Process).to receive(:kill).with('SIGKILL', pid).ordered - - run - end - end - - context 'when MAX_RSS is not exceeded' do - before do - stub_const("#{described_class}::MAX_RSS", 15.kilobytes) - end - - it 'does nothing' do - expect(subject).not_to receive(:sleep) - - run - end - end -end diff --git a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb new file mode 100644 index 00000000000..0001795c3f0 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::SidekiqMiddleware::Shutdown do + subject { described_class.new } + + let(:pid) { Process.pid } + let(:worker) { double(:worker, class: 'TestWorker') } + let(:job) { { 'jid' => 123 } } + let(:queue) { 'test_queue' } + let(:block) { proc { nil } } + + def run + subject.call(worker, job, queue) { block.call } + described_class.shutdown_thread&.join + end + + def pop_trace + subject.trace.pop(true) + end + + before do + allow(subject).to receive(:get_rss).and_return(10.kilobytes) + described_class.clear_shutdown_thread + end + + context 'when MAX_RSS is set to 0' do + before do + stub_const("#{described_class}::MAX_RSS", 0) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end + + def expect_shutdown_sequence + expect(pop_trace).to eq([:sleep, 15 * 60]) + expect(pop_trace).to eq([:kill, 'SIGTSTP', pid]) + + expect(pop_trace).to eq([:sleep, 30]) + expect(pop_trace).to eq([:kill, 'SIGTERM', pid]) + + expect(pop_trace).to eq([:sleep, 10]) + expect(pop_trace).to eq([:kill, 'SIGKILL', pid]) + end + + context 'when MAX_RSS is exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 5.kilobytes) + end + + it 'sends the TSTP, TERM and KILL signals at expected times' do + run + + expect_shutdown_sequence + end + end + + context 'when MAX_RSS is not exceeded' do + before do + stub_const("#{described_class}::MAX_RSS", 15.kilobytes) + end + + it 'does nothing' do + expect(subject).not_to receive(:sleep) + + run + end + end + + context 'when WantShutdown is raised' do + let(:block) { proc { raise described_class::WantShutdown } } + + it 'starts the shutdown sequence and re-raises the exception' do + expect { run }.to raise_exception(described_class::WantShutdown) + + # We can't expect 'run' to have joined on the shutdown thread, because + # it hit an exception. + shutdown_thread = described_class.shutdown_thread + expect(shutdown_thread).not_to be_nil + shutdown_thread.join + + expect_shutdown_sequence + end + end +end diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index 0173a45d480..e3447d974aa 100644 --- a/spec/lib/gitlab/slash_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' describe Gitlab::SlashCommands::Command do let(:project) { create(:project) } let(:user) { create(:user) } + let(:chat_name) { double(:chat_name, user: user) } describe '#execute' do subject do - described_class.new(project, user, params).execute + described_class.new(project, chat_name, params).execute end context 'when no command is available' do @@ -88,7 +89,7 @@ describe Gitlab::SlashCommands::Command do end describe '#match_command' do - subject { described_class.new(project, user, params).match_command.first } + subject { described_class.new(project, chat_name, params).match_command.first } context 'IssueShow is triggered' do let(:params) { { text: 'issue show 123' } } diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 74b5ef4bb26..0d57334aa4c 100644 --- a/spec/lib/gitlab/slash_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::Deploy do describe '#execute' do let(:project) { create(:project) } let(:user) { create(:user) } + let(:chat_name) { double(:chat_name, user: user) } let(:regex_match) { described_class.match('deploy staging to production') } before do @@ -16,7 +17,7 @@ describe Gitlab::SlashCommands::Deploy do end subject do - described_class.new(project, user).execute(regex_match) + described_class.new(project, chat_name).execute(regex_match) end context 'if no environment is defined' do diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 3b077c58c50..8e7df946529 100644 --- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::IssueNew do describe '#execute' do let(:project) { create(:project) } let(:user) { create(:user) } + let(:chat_name) { double(:chat_name, user: user) } let(:regex_match) { described_class.match("issue create bird is the word") } before do @@ -11,7 +12,7 @@ describe Gitlab::SlashCommands::IssueNew do end subject do - described_class.new(project, user).execute(regex_match) + described_class.new(project, chat_name).execute(regex_match) end context 'without description' do diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index 35d01efc1bd..189e9592f1b 100644 --- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -6,10 +6,11 @@ describe Gitlab::SlashCommands::IssueSearch do let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } let(:project) { create(:project) } let(:user) { create(:user) } + let(:chat_name) { double(:chat_name, user: user) } let(:regex_match) { described_class.match("issue search find") } subject do - described_class.new(project, user).execute(regex_match) + described_class.new(project, chat_name).execute(regex_match) end context 'when the user has no access' do diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb index e5834d5a2ee..b1db1638237 100644 --- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::SlashCommands::IssueShow do let(:issue) { create(:issue, project: project) } let(:project) { create(:project) } let(:user) { issue.author } + let(:chat_name) { double(:chat_name, user: user) } let(:regex_match) { described_class.match("issue show #{issue.iid}") } before do @@ -12,7 +13,7 @@ describe Gitlab::SlashCommands::IssueShow do end subject do - described_class.new(project, user).execute(regex_match) + described_class.new(project, chat_name).execute(regex_match) end context 'the issue exists' do diff --git a/spec/migrations/schedule_build_stage_migration_spec.rb b/spec/migrations/schedule_build_stage_migration_spec.rb new file mode 100644 index 00000000000..06657410173 --- /dev/null +++ b/spec/migrations/schedule_build_stage_migration_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration') + +describe ScheduleBuildStageMigration, :migration do + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:stages) { table(:ci_stages) } + let(:jobs) { table(:ci_builds) } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + + projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce') + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test') + + jobs.create!(id: 11, commit_id: 1, project_id: 123, stage_id: nil) + jobs.create!(id: 206, commit_id: 1, project_id: 123, stage_id: nil) + jobs.create!(id: 3413, commit_id: 1, project_id: 123, stage_id: nil) + jobs.create!(id: 4109, commit_id: 1, project_id: 123, stage_id: 1) + end + + it 'schedules delayed background migrations in batches in bulk' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 11, 11) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 206, 206) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 3413, 3413) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end +end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb index e89e534d914..504bc710b25 100644 --- a/spec/models/chat_name_spec.rb +++ b/spec/models/chat_name_spec.rb @@ -14,4 +14,24 @@ describe ChatName do it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) } it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) } + + describe '#update_last_used_at', :clean_gitlab_redis_shared_state do + it 'updates the last_used_at timestamp' do + expect(subject.last_used_at).to be_nil + + subject.update_last_used_at + + expect(subject.last_used_at).to be_present + end + + it 'does not update last_used_at if it was recently updated' do + subject.update_last_used_at + + time = subject.last_used_at + + subject.update_last_used_at + + expect(subject.last_used_at).to eq(time) + end + end end diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb index 145189e7469..1b10501701c 100644 --- a/spec/models/ci/group_variable_spec.rb +++ b/spec/models/ci/group_variable_spec.rb @@ -5,7 +5,7 @@ describe Ci::GroupVariable do it { is_expected.to include_module(HasVariable) } it { is_expected.to include_module(Presentable) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) } describe '.unprotected' do subject { described_class.unprotected } diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index e4ff551151e..875e8b2b682 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -6,7 +6,7 @@ describe Ci::Variable do describe 'validations' do it { is_expected.to include_module(HasVariable) } it { is_expected.to include_module(Presentable) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) } end describe '.unprotected' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index e6d7b9fde02..d1569e5d650 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1417,7 +1417,7 @@ describe API::Issues do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/12345/issues/#{issue.iid}/move", user), + post api("/projects/0/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_gitlab_http_status(404) @@ -1428,7 +1428,7 @@ describe API::Issues do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - to_project_id: 12345 + to_project_id: 0 expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 0e745c82395..11b5469be7b 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1218,7 +1218,7 @@ describe API::V3::Issues do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post v3_api("/projects/123/issues/#{issue.id}/move", user), + post v3_api("/projects/0/issues/#{issue.id}/move", user), to_project_id: target_project.id expect(response).to have_gitlab_http_status(404) @@ -1229,7 +1229,7 @@ describe API::V3::Issues do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), - to_project_id: 123 + to_project_id: 0 expect(response).to have_gitlab_http_status(404) end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index c6fdda203ad..1b0a5eac9b0 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -597,7 +597,7 @@ describe 'Git HTTP requests' do context "when a gitlab ci token is provided" do let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, :running) } - let(:other_project) { create(:project) } + let(:other_project) { create(:project, :repository) } before do build.update!(project: project) # can't associate it on factory create @@ -648,10 +648,10 @@ describe 'Git HTTP requests' do context 'when the repo does not exist' do let(:project) { create(:project) } - it 'rejects pulls with 403 Forbidden' do + it 'rejects pulls with 404 Not Found' do clone_get path, env - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:not_found) expect(response.body).to eq(git_access_error(:no_repo)) end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index de829011e58..6bed8e812c0 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -68,10 +68,10 @@ describe 'OpenID Connect requests' do let!(:public_email) { build :email, email: 'public@example.com' } let!(:private_email) { build :email, email: 'private@example.com' } - let!(:group1) { create :group, path: 'group1' } - let!(:group2) { create :group, path: 'group2' } - let!(:group3) { create :group, path: 'group3', parent: group2 } - let!(:group4) { create :group, path: 'group4', parent: group3 } + let!(:group1) { create :group } + let!(:group2) { create :group } + let!(:group3) { create :group, parent: group2 } + let!(:group4) { create :group, parent: group3 } before do group1.add_user(user, GroupMember::OWNER) @@ -93,8 +93,8 @@ describe 'OpenID Connect requests' do 'groups' => anything })) - expected_groups = %w[group1 group2/group3] - expected_groups << 'group2/group3/group4' if Group.supports_nested_groups? + expected_groups = [group1.full_path, group3.full_path] + expected_groups << group4.full_path if Group.supports_nested_groups? expect(json_response['groups']).to match_array(expected_groups) end end diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index 79aaac3aeb6..5734b10109a 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ChatNames::FindUserService do +describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do describe '#execute' do let(:service) { create(:service) } @@ -13,21 +13,30 @@ describe ChatNames::FindUserService do context 'when existing user is requested' do let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } } - it 'returns the existing user' do - is_expected.to eq(user) + it 'returns the existing chat_name' do + is_expected.to eq(chat_name) end - it 'updates when last time chat name was used' do + it 'updates the last used timestamp if one is not already set' do expect(chat_name.last_used_at).to be_nil subject - initial_last_used = chat_name.reload.last_used_at - expect(initial_last_used).to be_present + expect(chat_name.reload.last_used_at).to be_present + end + + it 'only updates an existing timestamp once within a certain time frame' do + service = described_class.new(service, params) + + expect(chat_name.last_used_at).to be_nil + + service.execute + + time = chat_name.reload.last_used_at - Timecop.travel(2.days.from_now) { described_class.new(service, params).execute } + service.execute - expect(chat_name.reload.last_used_at).to be > initial_last_used + expect(chat_name.reload.last_used_at).to eq(time) end end diff --git a/spec/services/ci/create_trace_artifact_service_spec.rb b/spec/services/ci/create_trace_artifact_service_spec.rb index 847a88920fe..8c5e8e438c7 100644 --- a/spec/services/ci/create_trace_artifact_service_spec.rb +++ b/spec/services/ci/create_trace_artifact_service_spec.rb @@ -4,40 +4,60 @@ describe Ci::CreateTraceArtifactService do describe '#execute' do subject { described_class.new(nil, nil).execute(job) } - let(:job) { create(:ci_build) } - context 'when the job does not have trace artifact' do context 'when the job has a trace file' do - before do - allow_any_instance_of(Gitlab::Ci::Trace) - .to receive(:default_path) { expand_fixture_path('trace/sample_trace') } + let!(:job) { create(:ci_build, :trace_live) } + let!(:legacy_path) { job.trace.read { |stream| return stream.path } } + let!(:legacy_checksum) { Digest::SHA256.file(legacy_path).hexdigest } + let(:new_path) { job.job_artifacts_trace.file.path } + let(:new_checksum) { Digest::SHA256.file(new_path).hexdigest } - allow_any_instance_of(JobArtifactUploader).to receive(:move_to_cache) { false } - allow_any_instance_of(JobArtifactUploader).to receive(:move_to_store) { false } - end + it { expect(File.exist?(legacy_path)).to be_truthy } it 'creates trace artifact' do expect { subject }.to change { Ci::JobArtifact.count }.by(1) - expect(job.job_artifacts_trace.read_attribute(:file)).to eq('sample_trace') + expect(File.exist?(legacy_path)).to be_falsy + expect(File.exist?(new_path)).to be_truthy + expect(new_checksum).to eq(legacy_checksum) + expect(job.job_artifacts_trace.file.exists?).to be_truthy + expect(job.job_artifacts_trace.file.filename).to eq('job.log') end - context 'when the job has already had trace artifact' do + context 'when failed to create trace artifact record' do before do - create(:ci_job_artifact, :trace, job: job) + # When ActiveRecord error happens + allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false) + allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages) + .and_return("Error") + + subject rescue nil + + job.reload end - it 'does not create trace artifact' do - expect { subject }.not_to change { Ci::JobArtifact.count } + it 'keeps legacy trace and removes trace artifact' do + expect(File.exist?(legacy_path)).to be_truthy + expect(job.job_artifacts_trace).to be_nil end end end context 'when the job does not have a trace file' do + let!(:job) { create(:ci_build) } + it 'does not create trace artifact' do expect { subject }.not_to change { Ci::JobArtifact.count } end end end + + context 'when the job has already had trace artifact' do + let!(:job) { create(:ci_build, :trace_artifact) } + + it 'does not create trace artifact' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index 78aa5d442e7..68d5660445a 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -15,47 +15,79 @@ describe Labels::FindOrCreateService do context 'when acting on behalf of a specific user' do let(:user) { create(:user) } - subject(:service) { described_class.new(user, project, params) } - before do - project.add_developer(user) - end - context 'when label does not exist at group level' do - it 'creates a new label at project level' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when finding labels on project level' do + subject(:service) { described_class.new(user, project, params) } + + before do + project.add_developer(user) end - end - context 'when label exists at group level' do - it 'returns the group label' do - group_label = create(:group_label, group: group, title: 'Security') + context 'when label does not exist at group level' do + it 'creates a new label at project level' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end - expect(service.execute).to eq group_label + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') + + expect(service.execute).to eq group_label + end + end + + context 'when label exists at project level' do + it 'returns the project label' do + project_label = create(:label, project: project, title: 'Security') + + expect(service.execute).to eq project_label + end end end - context 'when label does not exist at group level' do - it 'creates a new label at project leve' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when finding labels on group level' do + subject(:service) { described_class.new(user, group, params) } + + before do + group.add_developer(user) + end + + context 'when label does not exist at group level' do + it 'creates a new label at group level' do + expect { service.execute }.to change(group.labels, :count).by(1) + end + end + + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') + + expect(service.execute).to eq group_label + end end end + end + + context 'when authorization is not required' do + context 'when finding labels on project level' do + subject(:service) { described_class.new(nil, project, params) } - context 'when label exists at project level' do it 'returns the project label' do project_label = create(:label, project: project, title: 'Security') - expect(service.execute).to eq project_label + expect(service.execute(skip_authorization: true)).to eq project_label end end - end - context 'when authorization is not required' do - subject(:service) { described_class.new(nil, project, params) } + context 'when finding labels on group level' do + subject(:service) { described_class.new(nil, group, params) } - it 'returns the project label' do - project_label = create(:label, project: project, title: 'Security') + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') - expect(service.execute(skip_authorization: true)).to eq project_label + expect(service.execute(skip_authorization: true)).to eq group_label + end end end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index b3018169a1c..7076571b753 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -1,70 +1,56 @@ require 'spec_helper' describe Members::ApproveAccessRequestService do - let(:user) { create(:user) } - let(:access_requester) { create(:user) } let(:project) { create(:project, :public, :access_requestable) } let(:group) { create(:group, :public, :access_requestable) } + let(:current_user) { create(:user) } + let(:access_requester_user) { create(:user) } + let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) } let(:opts) { {} } shared_examples 'a service raising ActiveRecord::RecordNotFound' do it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service approving an access request' do it 'succeeds' do - expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1) + expect { described_class.new(current_user).execute(access_requester, opts) }.to change { source.requesters.count }.by(-1) end it 'returns a <Source>Member' do - member = described_class.new(source, user, params).execute(opts) + member = described_class.new(current_user).execute(access_requester, opts) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_nil end context 'with a custom access level' do - let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) } - it 'returns a ProjectMember with the custom access level' do - member = described_class.new(source, user, params2).execute(opts) + member = described_class.new(current_user, access_level: Gitlab::Access::MASTER).execute(access_requester, opts) - expect(member.access_level).to eq Gitlab::Access::MASTER + expect(member.access_level).to eq(Gitlab::Access::MASTER) end end end - context 'when no access requester are found' do - let(:params) { { user_id: 42 } } - - it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do - let(:source) { project } - end - - it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do - let(:source) { group } - end - end - context 'when an access requester is found' do before do - project.request_access(access_requester) - group.request_access(access_requester) + project.request_access(access_requester_user) + group.request_access(access_requester_user) end - let(:params) { { user_id: access_requester.id } } context 'when current user is nil' do let(:user) { nil } - context 'and :force option is not given' do + context 'and :ldap option is not given' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { project } end @@ -74,8 +60,8 @@ describe Members::ApproveAccessRequestService do end end - context 'and :force option is false' do - let(:opts) { { force: false } } + context 'and :skip_authorization option is false' do + let(:opts) { { skip_authorization: false } } it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { project } @@ -86,8 +72,8 @@ describe Members::ApproveAccessRequestService do end end - context 'and :force option is true' do - let(:opts) { { force: true } } + context 'and :skip_authorization option is true' do + let(:opts) { { skip_authorization: true } } it_behaves_like 'a service approving an access request' do let(:source) { project } @@ -97,18 +83,6 @@ describe Members::ApproveAccessRequestService do let(:source) { group } end end - - context 'and :force param is true' do - let(:params) { { user_id: access_requester.id, force: true } } - - it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do - let(:source) { project } - end - - it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do - let(:source) { group } - end - end end context 'when current user cannot approve access request to the project' do @@ -123,8 +97,8 @@ describe Members::ApproveAccessRequestService do context 'when current user can approve access request to the project' do before do - project.add_master(user) - group.add_owner(user) + project.add_master(current_user) + group.add_owner(current_user) end it_behaves_like 'a service approving an access request' do @@ -134,14 +108,6 @@ describe Members::ApproveAccessRequestService do it_behaves_like 'a service approving an access request' do let(:source) { group } end - - context 'when given a :id' do - let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } } - - it_behaves_like 'a service approving an access request' do - let(:source) { project } - end - end end end end diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb deleted file mode 100644 index 9cf6f64a078..00000000000 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'spec_helper' - -describe Members::AuthorizedDestroyService do - let(:member_user) { create(:user) } - let(:project) { create(:project, :public) } - let(:group) { create(:group, :public) } - let(:group_project) { create(:project, :public, group: group) } - - def number_of_assigned_issuables(user) - Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count - end - - context 'Invited users' do - # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504 - it 'destroys invited project member' do - project.add_developer(member_user) - - member = create :project_member, :invited, project: project - - expect { described_class.new(member, member_user).execute } - .to change { Member.count }.from(3).to(2) - end - - it "doesn't destroy invited project member notification_settings" do - project.add_developer(member_user) - - member = create :project_member, :invited, project: project - - expect { described_class.new(member, member_user).execute } - .not_to change { NotificationSetting.count } - end - - it 'destroys invited group member' do - group.add_developer(member_user) - - member = create :group_member, :invited, group: group - - expect { described_class.new(member, member_user).execute } - .to change { Member.count }.from(2).to(1) - end - - it "doesn't destroy invited group member notification_settings" do - group.add_developer(member_user) - - member = create :group_member, :invited, group: group - - expect { described_class.new(member, member_user).execute } - .not_to change { NotificationSetting.count } - end - end - - context 'Requested user' do - it "doesn't destroy member notification_settings" do - member = create(:project_member, user: member_user, requested_at: Time.now) - - expect { described_class.new(member, member_user).execute } - .not_to change { NotificationSetting.count } - end - end - - context 'Group member' do - let(:member) { group.members.find_by(user_id: member_user.id) } - - before do - group.add_developer(member_user) - end - - it "unassigns issues and merge requests" do - issue = create :issue, project: group_project, assignees: [member_user] - create :issue, assignees: [member_user] - merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user - create :merge_request, target_project: project, source_project: project, assignee: member_user - - expect { described_class.new(member, member_user).execute } - .to change { number_of_assigned_issuables(member_user) }.from(4).to(2) - - expect(issue.reload.assignee_ids).to be_empty - expect(merge_request.reload.assignee_id).to be_nil - end - - it 'destroys member notification_settings' do - group.add_developer(member_user) - member = group.members.find_by(user_id: member_user.id) - - expect { described_class.new(member, member_user).execute } - .to change { member_user.notification_settings.count }.by(-1) - end - end - - context 'Project member' do - let(:member) { project.members.find_by(user_id: member_user.id) } - - before do - project.add_developer(member_user) - end - - it "unassigns issues and merge requests" do - create :issue, project: project, assignees: [member_user] - create :merge_request, target_project: project, source_project: project, assignee: member_user - - expect { described_class.new(member, member_user).execute } - .to change { number_of_assigned_issuables(member_user) }.from(2).to(0) - end - - it 'destroys member notification_settings' do - expect { described_class.new(member, member_user).execute } - .to change { member_user.notification_settings.count }.by(-1) - end - end -end diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 6bd4718e780..1831c62d788 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -11,7 +11,7 @@ describe Members::CreateService do it 'adds user to members' do params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } - result = described_class.new(project, user, params).execute + result = described_class.new(user, params).execute(project) expect(result[:status]).to eq(:success) expect(project.users).to include project_user @@ -19,7 +19,7 @@ describe Members::CreateService do it 'adds no user to members' do params = { user_ids: '', access_level: Gitlab::Access::GUEST } - result = described_class.new(project, user, params).execute + result = described_class.new(user, params).execute(project) expect(result[:status]).to eq(:error) expect(result[:message]).to be_present @@ -30,7 +30,7 @@ describe Members::CreateService do user_ids = 1.upto(101).to_a.join(',') params = { user_ids: user_ids, access_level: Gitlab::Access::GUEST } - result = described_class.new(project, user, params).execute + result = described_class.new(user, params).execute(project) expect(result[:status]).to eq(:error) expect(result[:message]).to be_present diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 91152df3ad9..10c264a90c5 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -1,112 +1,202 @@ require 'spec_helper' describe Members::DestroyService do - let(:user) { create(:user) } + let(:current_user) { create(:user) } let(:member_user) { create(:user) } - let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } + let(:group_project) { create(:project, :public, group: group) } + let(:opts) { {} } shared_examples 'a service raising ActiveRecord::RecordNotFound' do it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.new(current_user).execute(member) }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(current_user).execute(member) }.to raise_error(Gitlab::Access::AccessDeniedError) end end + def number_of_assigned_issuables(user) + Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count + end + shared_examples 'a service destroying a member' do it 'destroys the member' do - expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1) + expect { described_class.new(current_user).execute(member, opts) }.to change { member.source.members_and_requesters.count }.by(-1) end - context 'when the given member is an access requester' do - before do - source.members.find_by(user_id: member_user).destroy - source.update_attributes(request_access_enabled: true) - source.request_access(member_user) + it 'unassigns issues and merge requests' do + if member.invite? + expect { described_class.new(current_user).execute(member, opts) } + .not_to change { number_of_assigned_issuables(member_user) } + else + create :issue, assignees: [member_user] + issue = create :issue, project: group_project, assignees: [member_user] + merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user + + expect { described_class.new(current_user).execute(member, opts) } + .to change { number_of_assigned_issuables(member_user) }.from(3).to(1) + + expect(issue.reload.assignee_ids).to be_empty + expect(merge_request.reload.assignee_id).to be_nil end - let(:access_requester) { source.requesters.find_by(user_id: member_user) } + end - it_behaves_like 'a service raising ActiveRecord::RecordNotFound' + it 'destroys member notification_settings' do + if member_user.notification_settings.any? + expect { described_class.new(current_user).execute(member, opts) } + .to change { member_user.notification_settings.count }.by(-1) + else + expect { described_class.new(current_user).execute(member, opts) } + .not_to change { member_user.notification_settings.count } + end + end + end - %i[requesters all].each do |scope| - context "and #{scope} scope is passed" do - it 'destroys the access requester' do - expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1) - end + shared_examples 'a service destroying an access requester' do + it_behaves_like 'a service destroying a member' - it 'calls Member#after_decline_request' do - expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester) + it 'calls Member#after_decline_request' do + expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) - described_class.new(source, user, params).execute(scope) - end + described_class.new(current_user).execute(member) + end - context 'when current user is the member' do - it 'does not call Member#after_decline_request' do - expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester) + context 'when current user is the member' do + it 'does not call Member#after_decline_request' do + expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) - described_class.new(source, member_user, params).execute(scope) - end - end - end + described_class.new(member_user).execute(member) end end end - context 'when no member are found' do - let(:params) { { user_id: 42 } } + context 'with a member' do + before do + group_project.add_developer(member_user) + group.add_developer(member_user) + end + + context 'when current user cannot destroy the given member' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:member) { group_project.members.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service destroying a member' do + let(:opts) { { skip_authorization: true } } + let(:member) { group_project.members.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:member) { group.members.find_by(user_id: member_user.id) } + end - it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do - let(:source) { project } + it_behaves_like 'a service destroying a member' do + let(:opts) { { skip_authorization: true } } + let(:member) { group.members.find_by(user_id: member_user.id) } + end end - it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do - let(:source) { group } + context 'when current user can destroy the given member' do + before do + group_project.add_master(current_user) + group.add_owner(current_user) + end + + it_behaves_like 'a service destroying a member' do + let(:member) { group_project.members.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service destroying a member' do + let(:member) { group.members.find_by(user_id: member_user.id) } + end end end - context 'when a member is found' do + context 'with an access requester' do before do - project.add_developer(member_user) - group.add_developer(member_user) + group_project.update_attributes(request_access_enabled: true) + group.update_attributes(request_access_enabled: true) + group_project.request_access(member_user) + group.request_access(member_user) end - let(:params) { { user_id: member_user.id } } - context 'when current user cannot destroy the given member' do + context 'when current user cannot destroy the given access requester' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do - let(:source) { project } + let(:member) { group_project.requesters.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service destroying a member' do + let(:opts) { { skip_authorization: true } } + let(:member) { group_project.requesters.find_by(user_id: member_user.id) } end it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do - let(:source) { group } + let(:member) { group.requesters.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service destroying a member' do + let(:opts) { { skip_authorization: true } } + let(:member) { group.requesters.find_by(user_id: member_user.id) } end end - context 'when current user can destroy the given member' do + context 'when current user can destroy the given access requester' do before do - project.add_master(user) - group.add_owner(user) + group_project.add_master(current_user) + group.add_owner(current_user) + end + + it_behaves_like 'a service destroying an access requester' do + let(:member) { group_project.requesters.find_by(user_id: member_user.id) } + end + + it_behaves_like 'a service destroying an access requester' do + let(:member) { group.requesters.find_by(user_id: member_user.id) } + end + end + end + + context 'with an invited user' do + let(:project_invited_member) { create(:project_member, :invited, project: group_project) } + let(:group_invited_member) { create(:group_member, :invited, group: group) } + + context 'when current user cannot destroy the given invited user' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:member) { project_invited_member } end it_behaves_like 'a service destroying a member' do - let(:source) { project } + let(:opts) { { skip_authorization: true } } + let(:member) { project_invited_member } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:member) { group_invited_member } end it_behaves_like 'a service destroying a member' do - let(:source) { group } + let(:opts) { { skip_authorization: true } } + let(:member) { group_invited_member } end + end - context 'when given a :id' do - let(:params) { { id: project.members.find_by!(user_id: user.id).id } } + context 'when current user can destroy the given invited user' do + before do + group_project.add_master(current_user) + group.add_owner(current_user) + end - it 'destroys the member' do - expect { described_class.new(project, user, params).execute } - .to change { project.members.count }.by(-1) - end + # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504 + it_behaves_like 'a service destroying a member' do + let(:member) { project_invited_member } + end + + it_behaves_like 'a service destroying a member' do + let(:member) { group_invited_member } end end end diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb index 0a704bba521..e93ba5a85c0 100644 --- a/spec/services/members/request_access_service_spec.rb +++ b/spec/services/members/request_access_service_spec.rb @@ -5,17 +5,17 @@ describe Members::RequestAccessService do shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(user).execute(source) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service creating a access request' do it 'succeeds' do - expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1) + expect { described_class.new(user).execute(source) }.to change { source.requesters.count }.by(1) end it 'returns a <Source>Member' do - member = described_class.new(source, user).execute + member = described_class.new(user).execute(source) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_present diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb new file mode 100644 index 00000000000..a451272dd1f --- /dev/null +++ b/spec/services/members/update_service_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Members::UpdateService do + let(:project) { create(:project, :public) } + let(:group) { create(:group, :public) } + let(:current_user) { create(:user) } + let(:member_user) { create(:user) } + let(:permission) { :update } + let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) } + let(:params) do + { access_level: Gitlab::Access::MASTER } + end + + shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do + it 'raises Gitlab::Access::AccessDeniedError' do + expect { described_class.new(current_user, params).execute(member, permission: permission) } + .to raise_error(Gitlab::Access::AccessDeniedError) + end + end + + shared_examples 'a service updating a member' do + it 'updates the member' do + updated_member = described_class.new(current_user, params).execute(member, permission: permission) + + expect(updated_member).to be_valid + expect(updated_member.access_level).to eq(Gitlab::Access::MASTER) + end + end + + before do + project.add_developer(member_user) + group.add_developer(member_user) + end + + context 'when current user cannot update the given member' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'when current user can update the given member' do + before do + project.add_master(current_user) + group.add_owner(current_user) + end + + it_behaves_like 'a service updating a member' do + let(:source) { project } + end + + it_behaves_like 'a service updating a member' do + let(:source) { group } + end + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 5189c57b7db..8603b7f3e2c 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -78,8 +78,10 @@ RSpec.configure do |config| end config.after(:example, :js) do |example| - # prevent localstorage from introducing side effects based on test order - execute_script("localStorage.clear();") + # prevent localStorage from introducing side effects based on test order + unless ['', 'about:blank', 'data:,'].include? Capybara.current_session.driver.browser.current_url + execute_script("localStorage.clear();") + end # capybara/rspec already calls Capybara.reset_sessions! in an `after` hook, # but `block_and_wait_for_requests_complete` is called before it so by diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 1809ae1d141..5edc5de2a09 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -28,11 +28,11 @@ RSpec.configure do |config| end config.before(:each, :js) do - DatabaseCleaner.strategy = :deletion + DatabaseCleaner.strategy = :deletion, { cache_tables: false } end config.before(:each, :delete) do - DatabaseCleaner.strategy = :deletion + DatabaseCleaner.strategy = :deletion, { cache_tables: false } end config.before(:each, :migration) do diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index 0d8f7a7aae6..f7f851eb1eb 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -261,6 +261,8 @@ shared_examples 'variable list' do click_button('Save variables') wait_for_requests + expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1) + # We check the first row because it re-sorts to alphabetical order on refresh page.within('.js-ci-variable-list-section') do expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 0d6eb536c33..d095138f6b7 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -1,79 +1,6 @@ require 'spec_helper' describe AuthorizedProjectsWorker do - let(:project) { create(:project) } - - def build_args_list(*ids, multiply: 1) - args_list = ids.map { |id| [id] } - args_list * multiply - end - - describe '.bulk_perform_and_wait' do - it 'schedules the ids and waits for the jobs to complete' do - args_list = build_args_list(project.owner.id) - - project.owner.project_authorizations.delete_all - described_class.bulk_perform_and_wait(args_list) - - expect(project.owner.project_authorizations.count).to eq(1) - end - - it 'inlines workloads <= 3 jobs' do - args_list = build_args_list(project.owner.id, multiply: 3) - expect(described_class).to receive(:bulk_perform_inline).with(args_list) - - described_class.bulk_perform_and_wait(args_list) - end - - it 'runs > 3 jobs using sidekiq' do - project.owner.project_authorizations.delete_all - - expect(described_class).to receive(:bulk_perform_async).and_call_original - - args_list = build_args_list(project.owner.id, multiply: 4) - described_class.bulk_perform_and_wait(args_list) - - expect(project.owner.project_authorizations.count).to eq(1) - end - end - - describe '.bulk_perform_inline' do - it 'refreshes the authorizations inline' do - project.owner.project_authorizations.delete_all - - expect_any_instance_of(described_class).to receive(:perform).and_call_original - - described_class.bulk_perform_inline(build_args_list(project.owner.id)) - - expect(project.owner.project_authorizations.count).to eq(1) - end - - it 'enqueues jobs if an error is raised' do - invalid_id = -1 - args_list = build_args_list(project.owner.id, invalid_id) - - allow_any_instance_of(described_class).to receive(:perform).with(project.owner.id) - allow_any_instance_of(described_class).to receive(:perform).with(invalid_id).and_raise(ArgumentError) - expect(described_class).to receive(:bulk_perform_async).with(build_args_list(invalid_id)) - - described_class.bulk_perform_inline(args_list) - end - end - - describe '.bulk_perform_async' do - it "uses it's respective sidekiq queue" do - args_list = build_args_list(project.owner.id) - push_bulk_args = { - 'class' => described_class, - 'args' => args_list - } - - expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once - - described_class.bulk_perform_async(args_list) - end - end - describe '#perform' do let(:user) { create(:user) } @@ -85,12 +12,6 @@ describe AuthorizedProjectsWorker do job.perform(user.id) end - it 'notifies the JobWaiter when done if the key is provided' do - expect(Gitlab::JobWaiter).to receive(:notify).with('notify-key', job.jid) - - job.perform(user.id, 'notify-key') - end - context "when the user is not found" do it "does nothing" do expect_any_instance_of(User).not_to receive(:refresh_authorized_projects) diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb new file mode 100644 index 00000000000..4af0de86ac9 --- /dev/null +++ b/spec/workers/concerns/waitable_worker_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe WaitableWorker do + let(:worker) do + Class.new do + def self.name + 'Gitlab::Foo::Bar::DummyWorker' + end + + class << self + cattr_accessor(:counter) { 0 } + end + + include ApplicationWorker + prepend WaitableWorker + + def perform(i = 0) + self.class.counter += i + end + end + end + + subject(:job) { worker.new } + + describe '.bulk_perform_and_wait' do + it 'schedules the jobs and waits for them to complete' do + worker.bulk_perform_and_wait([[1], [2]]) + + expect(worker.counter).to eq(3) + end + + it 'inlines workloads <= 3 jobs' do + args_list = [[1], [2], [3]] + expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original + + worker.bulk_perform_and_wait(args_list) + + expect(worker.counter).to eq(6) + end + + it 'runs > 3 jobs using sidekiq' do + expect(worker).to receive(:bulk_perform_async) + + worker.bulk_perform_and_wait([[1], [2], [3], [4]]) + end + end + + describe '.bulk_perform_inline' do + it 'runs the jobs inline' do + expect(worker).not_to receive(:bulk_perform_async) + + worker.bulk_perform_inline([[1], [2]]) + + expect(worker.counter).to eq(3) + end + + it 'enqueues jobs if an error is raised' do + expect(worker).to receive(:bulk_perform_async).with([['foo']]) + + worker.bulk_perform_inline([[1], ['foo']]) + end + end + + describe '#perform' do + shared_examples 'perform' do + it 'notifies the JobWaiter when done if the key is provided' do + key = Gitlab::JobWaiter.new.key + expect(Gitlab::JobWaiter).to receive(:notify).with(key, job.jid) + + job.perform(*args, key) + end + + it 'does not notify the JobWaiter when done if no key is provided' do + expect(Gitlab::JobWaiter).not_to receive(:notify) + + job.perform(*args) + end + end + + context 'when the worker takes arguments' do + let(:args) { [1] } + + it_behaves_like 'perform' + end + + context 'when the worker takes no arguments' do + let(:args) { [] } + + it_behaves_like 'perform' + end + end +end diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml index db967514be7..859f2ad82a4 100644 --- a/vendor/prometheus/values.yaml +++ b/vendor/prometheus/values.yaml @@ -10,6 +10,9 @@ nodeExporter: pushgateway: enabled: false +rbac: + create: false + server: image: tag: v2.1.0 diff --git a/yarn.lock b/yarn.lock index 4d7dc1be854..ab0ad265d81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8732,9 +8732,9 @@ webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0: range-parser "^1.0.3" time-stamp "^2.0.0" -webpack-dev-server@^2.11.1: - version "2.11.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0" +webpack-dev-server@^2.11.2: + version "2.11.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f" dependencies: ansi-html "0.0.7" array-includes "^3.0.3" |