diff options
327 files changed, 4386 insertions, 1951 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3163b687b4..0d70eae0d1e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -265,7 +265,7 @@ package-and-qa: SCRIPT_NAME: trigger-build-docs environment: name: review-docs/$CI_COMMIT_REF_SLUG - # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables + # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup diff --git a/.rubocop.yml b/.rubocop.yml index 5d2c5c7cf49..a95ded8af1c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -81,6 +81,9 @@ Naming/FileName: - 'locale/unfound_translations.rb' - 'ee/locale/unfound_translations.rb' - 'ee/lib/generators/**/*' + - 'qa/qa/scenario/test/integration/ldap_no_tls.rb' + - 'qa/qa/scenario/test/integration/ldap_tls.rb' + IgnoreExecutableScripts: true AllowedAcronyms: - EE diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc5b24aa39..4c99f6ed059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.4.3 (2018-10-26) + +- No changes. + +## 11.4.2 (2018-10-25) + +### Security (5 changes) + +- Escape entity title while autocomplete template rendering to prevent XSS. !2571 +- Persist only SHA digest of PersonalAccessToken#token. +- Redact personal tokens in unsubscribe links. +- Block loopback addresses in UrlBlocker. +- Validate Wiki attachments are valid temporary files. + + +## 11.4.1 (2018-10-23) + +### Security (2 changes) + +- Fix XSS in merge request source branch name. +- Prevent SSRF attacks in HipChat integration. + + ## 11.4.0 (2018-10-22) ### Security (9 changes) @@ -227,6 +250,22 @@ entry. - Check frozen string in style builds. (gfyoung) +## 11.3.8 (2018-10-27) + +- No changes. + +## 11.3.7 (2018-10-26) + +### Security (6 changes) + +- Escape entity title while autocomplete template rendering to prevent XSS. !2557 +- Persist only SHA digest of PersonalAccessToken#token. +- Fix XSS in merge request source branch name. +- Redact personal tokens in unsubscribe links. +- Prevent SSRF attacks in HipChat integration. +- Validate Wiki attachments are valid temporary files. + + ## 11.3.6 (2018-10-17) - No changes. @@ -516,6 +555,21 @@ entry. - Creates Vue component for artifacts block on job page. +## 11.2.7 (2018-10-27) + +- No changes. + +## 11.2.6 (2018-10-26) + +### Security (5 changes) + +- Escape entity title while autocomplete template rendering to prevent XSS. !2558 +- Fix XSS in merge request source branch name. +- Redact personal tokens in unsubscribe links. +- Persist only SHA digest of PersonalAccessToken#token. +- Prevent SSRF attacks in HipChat integration. + + ## 11.2.5 (2018-10-05) ### Security (3 changes) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 6085e946503..f0bb29e7638 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 9da0a092a0d..6da4de57dc6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.4.0
\ No newline at end of file +8.4.1 @@ -244,9 +244,6 @@ gem 'rack-attack', '~> 4.4.1' # Ace editor gem 'ace-rails-ap', '~> 4.1.0' -# Keyboard shortcuts -gem 'mousetrap-rails', '~> 1.4.6' - # Detect and convert string character encoding gem 'charlock_holmes', '~> 0.7.5' diff --git a/Gemfile.lock b/Gemfile.lock index e533b564d15..e755b0e0a8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -463,7 +463,6 @@ GEM mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) - mousetrap-rails (1.4.6) msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) @@ -547,7 +546,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -1046,7 +1045,6 @@ DEPENDENCIES method_source (~> 0.8) mini_magick minitest (~> 5.7.0) - mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap net-ssh (~> 5.0) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index b24911f3bb2..6ae7444f18f 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -466,7 +466,6 @@ GEM mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) - mousetrap-rails (1.4.6) msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) @@ -1055,7 +1054,6 @@ DEPENDENCIES method_source (~> 0.8) mini_magick minitest (~> 5.7.0) - mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap net-ssh (~> 5.0) diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 030288a1c9d..ae2d1ee3c6e 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import { Button } from '@gitlab-org/gitlab-ui'; +import { GlButton } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; @@ -10,7 +10,7 @@ export default { name: 'BoardNewIssue', components: { ProjectSelect, - 'gl-button': Button, + GlButton, }, props: { groupId: { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 3baac08d411..20665f903d5 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,12 +1,12 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { - 'gl-link': Link, + GlLink, Icon, }, data() { diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index 1089d0a72d3..c7a917457f3 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -47,7 +47,7 @@ export default class AjaxVariableList { } onSaveClicked() { - const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon'); + const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon'); loadingIcon.classList.toggle('hide', false); this.errorBox.classList.toggle('hide', true); // We use this to prevent a user from changing a key before we have a chance diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index 1411f7ffd5e..f60665577fe 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,17 +1,17 @@ import Vue from 'vue'; import { - Pagination, - ProgressBar, - Modal, - LoadingIcon, - ModalDirective, - TooltipDirective, + GlPagination, + GlProgressBar, + GlModal, + GlLoadingIcon, + GlModalDirective, + GlTooltipDirective, } from '@gitlab-org/gitlab-ui'; -Vue.component('gl-pagination', Pagination); -Vue.component('gl-progress-bar', ProgressBar); -Vue.component('gl-ui-modal', Modal); -Vue.component('gl-loading-icon', LoadingIcon); +Vue.component('gl-pagination', GlPagination); +Vue.component('gl-progress-bar', GlProgressBar); +Vue.component('gl-ui-modal', GlModal); +Vue.component('gl-loading-icon', GlLoadingIcon); -Vue.directive('gl-modal', ModalDirective); -Vue.directive('gl-tooltip', TooltipDirective); +Vue.directive('gl-modal', GlModalDirective); +Vue.directive('gl-tooltip', GlTooltipDirective); diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index a8d615dd8f0..59680959bb1 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -153,13 +153,9 @@ export default { }, setDiscussions() { if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) { - requestIdleCallback( - () => - this.assignDiscussionsToDiff().then(() => { - this.assignedDiscussions = true; - }), - { timeout: 1000 }, - ); + this.assignedDiscussions = true; + + requestIdleCallback(() => this.assignDiscussionsToDiff(), { timeout: 1000 }); } }, adjustView() { diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 9bbf62c0eb6..29b5aff0fb1 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -40,17 +40,14 @@ export default { comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, - isWhitespaceVisible() { - return !getParameterValues('w')[0]; - }, toggleWhitespaceText() { - if (this.isWhitespaceVisible) { + if (this.isWhitespaceVisible()) { return __('Hide whitespace changes'); } return __('Show whitespace changes'); }, toggleWhitespacePath() { - if (this.isWhitespaceVisible) { + if (this.isWhitespaceVisible()) { return mergeUrlParams({ w: 1 }, window.location.href); } @@ -67,6 +64,9 @@ export default { 'expandAllFiles', 'toggleShowTreeList', ]), + isWhitespaceVisible() { + return getParameterValues('w')[0] !== '1'; + }, }, }; </script> @@ -121,7 +121,7 @@ export default { </a> <a :href="toggleWhitespacePath" - class="btn btn-default" + class="btn btn-default qa-toggle-whitespace" > {{ toggleWhitespaceText }} </a> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 34e836a570a..96e7bd63183 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui'; +import { GlTooltipDirective } from '@gitlab-org/gitlab-ui'; import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; @@ -10,7 +10,7 @@ const treeListStorageKey = 'mr_diff_tree_list'; export default { directives: { - Tooltip, + GlTooltip: GlTooltipDirective, }, components: { Icon, @@ -101,7 +101,7 @@ export default { class="btn-group prepend-left-8 tree-list-view-toggle" > <button - v-tooltip.hover + v-gl-tooltip.hover :aria-label="__('List view')" :title="__('List view')" :class="{ @@ -116,7 +116,7 @@ export default { /> </button> <button - v-tooltip.hover + v-gl-tooltip.hover :aria-label="__('Tree view')" :title="__('Tree view')" :class="{ diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 5a8aebd2086..38a65f111a2 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -133,7 +133,7 @@ export default { }, right: { ...line.right, - discussions: right ? line.right.discussions.concat(discussion) : [], + discussions: right && !left ? line.right.discussions.concat(discussion) : [], }, }; } diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index a0797b594cb..26bec125445 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,14 +2,14 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ -import { Button } from '@gitlab-org/gitlab-ui'; +import { GlButton } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { components: { Icon, - 'gl-button': Button, + GlButton, }, directives: { tooltip, diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index b70125c80ca..e22f542b7bf 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -58,11 +58,16 @@ export const alternativeTokenKeys = [ export const conditions = [ { - url: 'assignee_id=0', + url: 'assignee_id=None', tokenKey: 'assignee', value: 'none', }, { + url: 'assignee_id=Any', + tokenKey: 'assignee', + value: 'any', + }, + { url: 'milestone_title=No+Milestone', tokenKey: 'milestone', value: 'none', diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 95636a9ccdd..7dd0efd622d 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -201,7 +201,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -267,7 +267,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -370,7 +370,7 @@ class GfmAutoComplete { displayTpl(value) { let tmpl = GfmAutoComplete.Loading.template; if (value.title != null) { - tmpl = GfmAutoComplete.Issues.template; + tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title); } return tmpl; }, @@ -557,8 +557,9 @@ GfmAutoComplete.Labels = { }; // Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { - // eslint-disable-next-line no-template-curly-in-string - template: '<li><small>${id}</small> ${title}</li>', + templateFunction(id, title) { + return `<li><small>${id}</small> ${_.escape(title)}</li>`; + }, }; // Milestones GfmAutoComplete.Milestones = { diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index dc84ee12f1e..d4c430cd2f3 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters } from 'vuex'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; @@ -13,7 +13,7 @@ import { activityBarViews } from '../constants'; export default { components: { - SkeletonLoading, + GlSkeletonLoading, ResizablePanel, ActivityBar, CommitSection, @@ -50,7 +50,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading /> + <gl-skeleton-loading /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e88f01fb4f4..d2ff55a4ee3 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -1,7 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import FileRow from '~/vue_shared/components/file_row.vue'; import NavDropdown from './nav_dropdown.vue'; import FileRowExtra from './file_row_extra.vue'; @@ -9,7 +9,7 @@ import FileRowExtra from './file_row_extra.vue'; export default { components: { Icon, - SkeletonLoading, + GlSkeletonLoading, NavDropdown, FileRow, }, @@ -51,7 +51,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading /> + <gl-skeleton-loading /> </div> </template> <template v-else> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 0a2681b7a1e..b670b0355b7 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -25,7 +25,7 @@ export default { ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']), ciLintText() { return sprintf( - __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'), + __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'), { linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`, linkEnd: '</a>', diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 67338aa96c3..98182d92c2f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -149,7 +149,7 @@ export default { .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), this.service .getEnvironmentsData() - .then((data) => this.store.storeEnvironmentsData(data)) + .then(data => this.store.storeEnvironmentsData(data)) .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))), ]) .then(() => { @@ -157,6 +157,7 @@ export default { this.state = 'noData'; return; } + this.showEmptyState = false; }) .then(this.resize) @@ -195,7 +196,10 @@ export default { name="chevron-down" /> </button> - <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> + <div + v-if="store.environmentsData.length > 0" + class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" + > <ul> <li v-for="environment in store.environmentsData" diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 1369b5820d5..90fe339e3de 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,7 +16,7 @@ import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import Vue from 'vue'; import syntaxHighlight from '~/syntax_highlight'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; @@ -1293,10 +1293,10 @@ export default class Notes { new Vue({ el, components: { - SkeletonLoading, + GlSkeletonLoading, }, render(createElement) { - return createElement('skeleton-loading'); + return createElement('gl-skeleton-loading'); }, }); } diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index d9e99603238..eaa0cded224 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -3,13 +3,13 @@ import { mapState, mapActions } from 'vuex'; import imageDiffHelper from '~/image_diff/helpers/index'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; export default { components: { DiffFileHeader, - SkeletonLoading, + GlSkeletonLoading, }, props: { discussion: { @@ -143,7 +143,7 @@ export default { class="line_content js-success-lazy-load" > <span></span> - <skeleton-loading /> + <gl-skeleton-loading /> <span></span> </td> </tr> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 27972682ca1..6e8f43048d1 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -22,9 +22,7 @@ export default { return { currentValue: this.defaultValue }; }, computed: { - ...mapGetters([ - 'getNotesDataByProp', - ]), + ...mapGetters(['getNotesDataByProp']), currentFilter() { if (!this.currentValue) return this.filters[0]; return this.filters.find(filter => filter.value === this.currentValue); @@ -51,7 +49,7 @@ export default { <button id="discussion-filter-dropdown" ref="dropdownToggle" - class="btn btn-default" + class="btn btn-default qa-discussion-filter" data-toggle="dropdown" aria-expanded="false" > @@ -69,6 +67,7 @@ export default { > <button :class="{ 'is-active': filter.value === currentValue }" + class="qa-filter-options" type="button" @click="selectFilter(filter.value)" > diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c68860d98ae..e707f44bf5a 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -95,29 +95,20 @@ export default { return awardList.filter(award => award.user.id === this.getUserData.id).length; }, awardTitle(awardsList) { - const hasReactionByCurrentUser = this.hasReactionByCurrentUser( - awardsList, - ); + const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; let awardList = awardsList; // Filter myself from list if I am awarded. if (hasReactionByCurrentUser) { - awardList = awardList.filter( - award => award.user.id !== this.getUserData.id, - ); + awardList = awardList.filter(award => award.user.id !== this.getUserData.id); } // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList - .slice(0, TOOLTIP_NAME_COUNT) - .map(award => award.user.name); + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); // Get the remaining list to use in `and x more` text. - const remainingAwardList = awardList.slice( - TOOLTIP_NAME_COUNT, - awardList.length, - ); + const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); // Add myself to the begining of the list so title will start with You. if (hasReactionByCurrentUser) { @@ -128,9 +119,7 @@ export default { // We have 10+ awarded user, join them with comma and add `and x more`. if (remainingAwardList.length) { - title = `${namesToShow.join(', ')}, and ${ - remainingAwardList.length - } more.`; + title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; } else if (namesToShow.length > 1) { // Join all names with comma but not the last one, it will be added with and text. title = namesToShow.slice(0, namesToShow.length - 1).join(', '); @@ -170,9 +159,7 @@ export default { awardName: parsedName, }; - this.toggleAwardRequest(data).catch(() => - Flash('Something went wrong on our end.'), - ); + this.toggleAwardRequest(data).catch(() => Flash('Something went wrong on our end.')); }, }, }; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index fa4a1c56b20..4532226aa07 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -68,10 +68,7 @@ export const collapseSystemNotes = notes => { lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; } else if (lastDescriptionSystemNote) { - const timeDifferenceMinutes = getTimeDifferenceMinutes( - lastDescriptionSystemNote, - note, - ); + const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); // are they less than 10 minutes appart? if (timeDifferenceMinutes > 10) { diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index f105b7d0d11..d41b02b4a4b 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -4,5 +4,4 @@ import notesModule from './modules'; Vue.use(Vuex); -export default () => - new Vuex.Store(notesModule()); +export default () => new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index d3b2656743d..ae0a8c74964 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 8f5ac3d8082..15c6fb550c1 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue index 760ea8fe1e6..7a558558c4d 100644 --- a/app/assets/javascripts/performance_bar/components/simple_metric.vue +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -1,29 +1,29 @@ <script> - export default { - props: { - currentRequest: { - type: Object, - required: true, - }, - metric: { - type: String, - required: true, - }, +export default { + props: { + currentRequest: { + type: Object, + required: true, }, - computed: { - duration() { - return ( - this.currentRequest.details[this.metric] && - this.currentRequest.details[this.metric].duration - ); - }, - calls() { - return ( - this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls - ); - }, + metric: { + type: String, + required: true, }, - }; + }, + computed: { + duration() { + return ( + this.currentRequest.details[this.metric] && + this.currentRequest.details[this.metric].duration + ); + }, + calls() { + return ( + this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls + ); + }, + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index 6e5ef0ac0b2..29bfb7ee5df 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -9,8 +9,7 @@ export default ({ container }) => performanceBarApp: () => import('./components/performance_bar_app.vue'), }, data() { - const performanceBarData = document.querySelector(this.$options.el) - .dataset; + const performanceBarData = document.querySelector(this.$options.el).dataset; const store = new PerformanceBarStore(); return { diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 60d9ba62570..3a496fa2ed8 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -11,8 +11,10 @@ export default class PerformanceBarService { static registerInterceptor(peekUrl, callback) { const interceptor = response => { - const [fireCallback, requestId, requestUrl] = - PerformanceBarService.callbackParams(response, peekUrl); + const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams( + response, + peekUrl, + ); if (fireCallback) { callback(requestId, requestUrl); @@ -30,10 +32,7 @@ export default class PerformanceBarService { static removeInterceptor(interceptor) { axios.interceptors.response.eject(interceptor); - Vue.http.interceptors = _.without( - Vue.http.interceptors, - vueResourceInterceptor, - ); + Vue.http.interceptors = _.without(Vue.http.interceptors, vueResourceInterceptor); } static callbackParams(response, peekUrl) { diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js index c6b2f55243c..031e774d533 100644 --- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -32,8 +32,6 @@ export default class PerformanceBarStore { } canTrackRequest(requestUrl) { - return ( - this.requests.filter(request => request.url === requestUrl).length < 2 - ); + return this.requests.filter(request => request.url === requestUrl).length < 2; } } diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 974629fa2af..99b57f4c9d5 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,78 +1,78 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { __, s__, sprintf } from '~/locale'; - import csrf from '~/lib/utils/csrf'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { __, s__, sprintf } from '~/locale'; +import csrf from '~/lib/utils/csrf'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + actionUrl: { + type: String, + required: true, }, - props: { - actionUrl: { - type: String, - required: true, - }, - confirmWithPassword: { - type: Boolean, - required: true, - }, - username: { - type: String, - required: true, - }, + confirmWithPassword: { + type: Boolean, + required: true, }, - data() { - return { - enteredPassword: '', - enteredUsername: '', - }; + username: { + type: String, + required: true, }, - computed: { - csrfToken() { - return csrf.token; - }, - inputLabel() { - let confirmationValue; - if (this.confirmWithPassword) { - confirmationValue = __('password'); - } else { - confirmationValue = __('username'); - } + }, + data() { + return { + enteredPassword: '', + enteredUsername: '', + }; + }, + computed: { + csrfToken() { + return csrf.token; + }, + inputLabel() { + let confirmationValue; + if (this.confirmWithPassword) { + confirmationValue = __('password'); + } else { + confirmationValue = __('username'); + } - confirmationValue = `<code>${confirmationValue}</code>`; + confirmationValue = `<code>${confirmationValue}</code>`; - return sprintf( - s__('Profiles|Type your %{confirmationValue} to confirm:'), - { confirmationValue }, - false, - ); - }, - text() { - return sprintf( - s__(`Profiles| + return sprintf( + s__('Profiles|Type your %{confirmationValue} to confirm:'), + { confirmationValue }, + false, + ); + }, + text() { + return sprintf( + s__(`Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), - { - yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, - deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, - }, - false, - ); - }, + { + yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, + deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, + }, + false, + ); }, - methods: { - canSubmit() { - if (this.confirmWithPassword) { - return this.enteredPassword !== ''; - } + }, + methods: { + canSubmit() { + if (this.confirmWithPassword) { + return this.enteredPassword !== ''; + } - return this.enteredUsername === this.username; - }, - onSubmit() { - this.$refs.form.submit(); - }, + return this.enteredUsername === this.username; + }, + onSubmit() { + this.$refs.form.submit(); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index af134881f31..befe91c332f 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -4,20 +4,35 @@ import $ from 'jquery'; import 'cropper'; import _ from 'underscore'; -((global) => { +(global => { // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; class GitLabCrop { - constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, - exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { + constructor( + input, + { + filename, + previewImage, + modalCrop, + pickImageEl, + uploadImageBtn, + modalCropImg, + exportWidth = 200, + exportHeight = 200, + cropBoxWidth = 200, + cropBoxHeight = 200, + } = {}, + ) { this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); this.onModalHide = this.onModalHide.bind(this); this.onModalShow = this.onModalShow.bind(this); this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; - this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `${this.fileInput.attr('id')}-trigger`); + this.fileInput + .attr('name', `${this.fileInput.attr('name')}-trigger`) + .attr('id', `${this.fileInput.attr('id')}-trigger`); this.exportWidth = exportWidth; this.exportHeight = exportHeight; this.cropBoxWidth = cropBoxWidth; @@ -59,7 +74,7 @@ import _ from 'underscore'; btn = this; return _this.onActionBtnClick(btn); }); - return this.croppedImageBlob = null; + return (this.croppedImageBlob = null); } onPickImageClick() { @@ -94,9 +109,9 @@ import _ from 'underscore'; width: cropBoxWidth, height: cropBoxHeight, left: (container.width - cropBoxWidth) / 2, - top: (container.height - cropBoxHeight) / 2 + top: (container.height - cropBoxHeight) / 2, }); - } + }, }); } @@ -116,7 +131,7 @@ import _ from 'underscore'; var data, result; data = $(btn).data(); if (this.modalCropImg.data('cropper') && data.method) { - return result = this.modalCropImg.cropper(data.method, data.option); + return (result = this.modalCropImg.cropper(data.method, data.option)); } } @@ -127,7 +142,7 @@ import _ from 'underscore'; readFile(input) { var _this, reader; _this = this; - reader = new FileReader; + reader = new FileReader(); reader.onload = () => { _this.modalCropImg.attr('src', reader.result); return _this.modalCrop.modal('show'); @@ -145,7 +160,7 @@ import _ from 'underscore'; array.push(binary.charCodeAt(i)); } return new Blob([new Uint8Array(array)], { - type: 'image/png' + type: 'image/png', }); } @@ -157,11 +172,13 @@ import _ from 'underscore'; } setBlob() { - this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', { - width: 200, - height: 200 - }).toDataURL('image/png'); - return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL); + this.dataURL = this.modalCropImg + .cropper('getCroppedCanvas', { + width: 200, + height: 200, + }) + .toDataURL('image/png'); + return (this.croppedImageBlob = this.dataURLtoBlob(this.dataURL)); } getBlob() { diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index e49c67ffb5c..8704a655b28 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -26,11 +26,7 @@ export default class Profile { } bindEvents() { - $('.js-preferences-form').on( - 'change.preference', - 'input[type=radio]', - this.submitForm, - ); + $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); $('#user_notified_of_own_activity').on('change', this.submitForm); this.form.on('submit', this.onSubmitForm); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index b601b19e7be..48343c8ba0a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -46,8 +46,12 @@ export default class ProtectedBranchCreate { onSelect() { // Enable submit button const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); - const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + const $allowedToMergeInput = this.$form.find( + 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]', + ); + const $allowedToPushInput = this.$form.find( + 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]', + ); const completedForm = !( $branchInput.val() && $allowedToMergeInput.length && diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 54560d08ad7..5bc08f60d16 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -29,8 +29,12 @@ export default class ProtectedBranchEdit { } onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + const $allowedToMergeInput = this.$wrap.find( + `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`, + ); + const $allowedToPushInput = this.$wrap.find( + `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; @@ -38,25 +42,36 @@ export default class ProtectedBranchEdit { this.$allowedToMergeDropdown.disable(); this.$allowedToPushDropdown.disable(); - axios.patch(this.$wrap.data('url'), { - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('accessLevelId'), - access_level: $allowedToMergeInput.val(), - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('accessLevelId'), - access_level: $allowedToPushInput.val(), - }], - }, - }).then(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - }).catch(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - - flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_branch: { + merge_access_levels_attributes: [ + { + id: this.$allowedToMergeDropdown.data('accessLevelId'), + access_level: $allowedToMergeInput.val(), + }, + ], + push_access_levels_attributes: [ + { + id: this.$allowedToPushDropdown.data('accessLevelId'), + access_level: $allowedToPushInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }) + .catch(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + + flash( + 'Failed to update branch!', + 'alert', + document.querySelector('.js-protected-branches-list'), + ); + }); } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 2f8116df0d2..fddf2674cbb 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -40,7 +40,9 @@ export default class ProtectedTagCreate { const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); - this.$form.find('input[type="submit"]').prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); + this.$form + .find('input[type="submit"]') + .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length)); } static getProtectedTags(term, callback) { diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 8687b2a4044..c52497e62f2 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -21,26 +21,33 @@ export default class ProtectedTagEdit { } onSelect() { - const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`); + const $allowedToCreateInput = this.$wrap.find( + `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`, + ); // Do not update if one dropdown has not selected any option if (!$allowedToCreateInput.length) return; this.$allowedToCreateDropdownButton.disable(); - axios.patch(this.$wrap.data('url'), { - protected_tag: { - create_access_levels_attributes: [{ - id: this.$allowedToCreateDropdownButton.data('accessLevelId'), - access_level: $allowedToCreateInput.val(), - }], - }, - }).then(() => { - this.$allowedToCreateDropdownButton.enable(); - }).catch(() => { - this.$allowedToCreateDropdownButton.enable(); - - flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); - }); + axios + .patch(this.$wrap.data('url'), { + protected_tag: { + create_access_levels_attributes: [ + { + id: this.$allowedToCreateDropdownButton.data('accessLevelId'), + access_level: $allowedToCreateInput.val(), + }, + ], + }, + }) + .then(() => { + this.$allowedToCreateDropdownButton.enable(); + }) + .catch(() => { + this.$allowedToCreateDropdownButton.enable(); + + flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); + }); } } diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 7e2287ac4db..9dd1c87a87d 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,42 +1,35 @@ <script> - import { mapGetters, mapActions } from 'vuex'; - import Flash from '../../flash'; - import store from '../stores'; - import collapsibleContainer from './collapsible_container.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; +import { mapGetters, mapActions } from 'vuex'; +import Flash from '../../flash'; +import store from '../stores'; +import collapsibleContainer from './collapsible_container.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; - export default { - name: 'RegistryListApp', - components: { - collapsibleContainer, +export default { + name: 'RegistryListApp', + components: { + collapsibleContainer, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - store, - computed: { - ...mapGetters([ - 'isLoading', - 'repos', - ]), - }, - created() { - this.setMainEndpoint(this.endpoint); - }, - mounted() { - this.fetchRepos() - .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); - }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, - }; + }, + store, + computed: { + ...mapGetters(['isLoading', 'repos']), + }, + created() { + this.setMainEndpoint(this.endpoint); + }, + mounted() { + this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); + }, + methods: { + ...mapActions(['setMainEndpoint', 'fetchRepos']), + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index d9bf41924d1..501b2625ae5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,62 +1,59 @@ <script> - import { mapActions } from 'vuex'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import tableRegistry from './table_registry.vue'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { __ } from '../../locale'; +import { mapActions } from 'vuex'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import tableRegistry from './table_registry.vue'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { __ } from '../../locale'; - export default { - name: 'CollapsibeContainerRegisty', - components: { - clipboardButton, - tableRegistry, +export default { + name: 'CollapsibeContainerRegisty', + components: { + clipboardButton, + tableRegistry, + }, + directives: { + tooltip, + }, + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - props: { - repo: { - type: Object, - required: true, - }, - }, - data() { - return { - isOpen: false, - }; - }, - methods: { - ...mapActions([ - 'fetchRepos', - 'fetchList', - 'deleteRepo', - ]), + }, + data() { + return { + isOpen: false, + }; + }, + methods: { + ...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']), - toggleRepo() { - this.isOpen = !this.isOpen; + toggleRepo() { + this.isOpen = !this.isOpen; - if (this.isOpen) { - this.fetchList({ repo: this.repo }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - } - }, + if (this.isOpen) { + this.fetchList({ repo: this.repo }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + } + }, - handleDeleteRepository() { - this.deleteRepo(this.repo) - .then(() => { - Flash(__('This container registry has been scheduled for deletion.'), 'notice'); - this.fetchRepos(); - }) - .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); - }, + handleDeleteRepository() { + this.deleteRepo(this.repo) + .then(() => { + Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + this.fetchRepos(); + }) + .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index fafb35bd69a..bb6c977fc63 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,66 +1,62 @@ <script> - import { mapActions } from 'vuex'; - import { n__ } from '../../locale'; - import Flash from '../../flash'; - import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import timeagoMixin from '../../vue_shared/mixins/timeago'; - import { errorMessages, errorMessagesTypes } from '../constants'; - import { numberToHumanSize } from '../../lib/utils/number_utils'; +import { mapActions } from 'vuex'; +import { n__ } from '../../locale'; +import Flash from '../../flash'; +import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import tablePagination from '../../vue_shared/components/table_pagination.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import { errorMessages, errorMessagesTypes } from '../constants'; +import { numberToHumanSize } from '../../lib/utils/number_utils'; - export default { - components: { - clipboardButton, - tablePagination, +export default { + components: { + clipboardButton, + tablePagination, + }, + directives: { + tooltip, + }, + mixins: [timeagoMixin], + props: { + repo: { + type: Object, + required: true, }, - directives: { - tooltip, + }, + computed: { + shouldRenderPagination() { + return this.repo.pagination.total > this.repo.pagination.perPage; }, - mixins: [ - timeagoMixin, - ], - props: { - repo: { - type: Object, - required: true, - }, - }, - computed: { - shouldRenderPagination() { - return this.repo.pagination.total > this.repo.pagination.perPage; - }, - }, - methods: { - ...mapActions([ - 'fetchList', - 'deleteRegistry', - ]), + }, + methods: { + ...mapActions(['fetchList', 'deleteRegistry']), - layers(item) { - return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; - }, + layers(item) { + return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; + }, - formatSize(size) { - return numberToHumanSize(size); - }, + formatSize(size) { + return numberToHumanSize(size); + }, - handleDeleteRegistry(registry) { - this.deleteRegistry(registry) - .then(() => this.fetchList({ repo: this.repo })) - .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); - }, + handleDeleteRegistry(registry) { + this.deleteRegistry(registry) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + }, - onPageChange(pageNumber) { - this.fetchList({ repo: this.repo, page: pageNumber }) - .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); - }, + onPageChange(pageNumber) { + this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => + this.showError(errorMessagesTypes.FETCH_REGISTRY), + ); + }, - showError(message) { - Flash(errorMessages[message]); - }, + showError(message) { + Flash(errorMessages[message]); }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index e15cd94a915..025afefe7f0 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -4,22 +4,23 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -export default () => new Vue({ - el: '#js-vue-registry-images', - components: { - registryApp, - }, - data() { - const { dataset } = document.querySelector(this.$options.el); - return { - endpoint: dataset.endpoint, - }; - }, - render(createElement) { - return createElement('registry-app', { - props: { - endpoint: this.endpoint, - }, - }); - }, -}); +export default () => + new Vue({ + el: '#js-vue-registry-images', + components: { + registryApp, + }, + data() { + const { dataset } = document.querySelector(this.$options.el); + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('registry-app', { + props: { + endpoint: this.endpoint, + }, + }); + }, + }); diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js index 208c3c39866..69c051cd2d6 100644 --- a/app/assets/javascripts/registry/stores/mutations.js +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -2,7 +2,6 @@ import * as types from './mutation_types'; import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default { - [types.SET_MAIN_ENDPOINT](state, endpoint) { Object.assign(state, { endpoint }); }, diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index b373d83a44b..bd204503cc7 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,79 +1,72 @@ <script> - import { mapActions, mapGetters, mapState } from 'vuex'; - import { s__ } from '~/locale'; - import { componentNames } from './issue_body'; - import ReportSection from './report_section.vue'; - import SummaryRow from './summary_row.vue'; - import IssuesList from './issues_list.vue'; - import Modal from './modal.vue'; - import createStore from '../store'; - import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { s__ } from '~/locale'; +import { componentNames } from './issue_body'; +import ReportSection from './report_section.vue'; +import SummaryRow from './summary_row.vue'; +import IssuesList from './issues_list.vue'; +import Modal from './modal.vue'; +import createStore from '../store'; +import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; - export default { - name: 'GroupedTestReportsApp', - store: createStore(), - components: { - ReportSection, - SummaryRow, - IssuesList, - Modal, +export default { + name: 'GroupedTestReportsApp', + store: createStore(), + components: { + ReportSection, + SummaryRow, + IssuesList, + Modal, + }, + props: { + endpoint: { + type: String, + required: true, }, - props: { - endpoint: { - type: String, - required: true, - }, - }, - componentNames, - computed: { - ...mapState([ - 'reports', - 'isLoading', - 'hasError', - 'summary', - ]), - ...mapState({ - modalTitle: state => state.modal.title || '', - modalData: state => state.modal.data || {}, - }), - ...mapGetters([ - 'summaryStatus', - ]), - groupedSummaryText() { - if (this.isLoading) { - return s__('Reports|Test summary results are being parsed'); - } + }, + componentNames, + computed: { + ...mapState(['reports', 'isLoading', 'hasError', 'summary']), + ...mapState({ + modalTitle: state => state.modal.title || '', + modalData: state => state.modal.data || {}, + }), + ...mapGetters(['summaryStatus']), + groupedSummaryText() { + if (this.isLoading) { + return s__('Reports|Test summary results are being parsed'); + } - if (this.hasError) { - return s__('Reports|Test summary failed loading results'); - } + if (this.hasError) { + return s__('Reports|Test summary failed loading results'); + } - return summaryTextBuilder(s__('Reports|Test summary'), this.summary); - }, + return summaryTextBuilder(s__('Reports|Test summary'), this.summary); }, - created() { - this.setEndpoint(this.endpoint); + }, + created() { + this.setEndpoint(this.endpoint); - this.fetchReports(); + this.fetchReports(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchReports']), + reportText(report) { + const summary = report.summary || {}; + return reportTextBuilder(report.name, summary); + }, + getReportIcon(report) { + return statusIcon(report.status); }, - methods: { - ...mapActions(['setEndpoint', 'fetchReports']), - reportText(report) { - const summary = report.summary || {}; - return reportTextBuilder(report.name, summary); - }, - getReportIcon(report) { - return statusIcon(report.status); - }, - shouldRenderIssuesList(report) { - return ( - report.existing_failures.length > 0 || - report.new_failures.length > 0 || - report.resolved_failures.length > 0 - ); - }, + shouldRenderIssuesList(report) { + return ( + report.existing_failures.length > 0 || + report.new_failures.length > 0 || + report.resolved_failures.length > 0 + ); }, - }; + }, +}; </script> <template> <report-section diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index 85811698a37..6e143c4f98c 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,10 +1,6 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import { - STATUS_FAILED, - STATUS_NEUTRAL, - STATUS_SUCCESS, -} from '../constants'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index df42201b5de..3b425ee2fed 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,6 @@ <script> import IssuesBlock from '~/reports/components/report_issues.vue'; -import { - STATUS_SUCCESS, - STATUS_FAILED, - STATUS_NEUTRAL, -} from '~/reports/constants'; +import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index acc5c6d85e2..5f9e4072b2d 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -1,27 +1,27 @@ <script> - import Modal from '~/vue_shared/components/gl_modal.vue'; - import LoadingButton from '~/vue_shared/components/loading_button.vue'; - import CodeBlock from '~/vue_shared/components/code_block.vue'; - import { fieldTypes } from '../constants'; +import Modal from '~/vue_shared/components/gl_modal.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import CodeBlock from '~/vue_shared/components/code_block.vue'; +import { fieldTypes } from '../constants'; - export default { - components: { - Modal, - LoadingButton, - CodeBlock, +export default { + components: { + Modal, + LoadingButton, + CodeBlock, + }, + props: { + title: { + type: String, + required: true, }, - props: { - title: { - type: String, - required: true, - }, - modalData: { - type: Object, - required: true, - }, + modalData: { + type: Object, + required: true, }, - fieldTypes, - }; + }, + fieldTypes, +}; </script> <template> <modal diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index cd443a49b52..1a87822fcc3 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -1,28 +1,28 @@ <script> - import { mapActions } from 'vuex'; +import { mapActions } from 'vuex'; - export default { - name: 'TestIssueBody', - props: { - issue: { - type: Object, - required: true, - }, - // failed || success - status: { - type: String, - required: true, - }, - isNew: { - type: Boolean, - required: false, - default: false, - }, +export default { + name: 'TestIssueBody', + props: { + issue: { + type: Object, + required: true, }, - methods: { - ...mapActions(['openModal']), + // failed || success + status: { + type: String, + required: true, }, - }; + isNew: { + type: Boolean, + required: false, + default: false, + }, + }, + methods: { + ...mapActions(['openModal']), + }, +}; </script> <template> <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js index acabcc1d193..db8ab5ccb80 100644 --- a/app/assets/javascripts/reports/store/actions.js +++ b/app/assets/javascripts/reports/store/actions.js @@ -43,9 +43,11 @@ export const fetchReports = ({ state, dispatch }) => { }, data: state.endpoint, method: 'getReports', - successCallback: ({ data, status }) => dispatch('receiveReportsSuccess', { - data, status, - }), + successCallback: ({ data, status }) => + dispatch('receiveReportsSuccess', { + data, + status, + }), errorCallback: () => dispatch('receiveReportsError'), }); diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js index 9d8f7dc3b74..467c692b438 100644 --- a/app/assets/javascripts/reports/store/index.js +++ b/app/assets/javascripts/reports/store/index.js @@ -7,9 +7,10 @@ import state from './state'; Vue.use(Vuex); -export default () => new Vuex.Store({ - actions, - mutations, - getters, - state: state(), -}); +export default () => + new Vuex.Store({ + actions, + mutations, + getters, + state: state(), + }); diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js index 82bda31df5d..599d4862dfe 100644 --- a/app/assets/javascripts/reports/store/mutation_types.js +++ b/app/assets/javascripts/reports/store/mutation_types.js @@ -4,4 +4,3 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS'; export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; - diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index b88bff97075..2a37f5b74fa 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -19,7 +19,6 @@ export default { state.status = response.status; state.reports = response.suites; - }, [types.RECEIVE_REPORTS_ERROR](state) { state.isLoading = false; @@ -36,7 +35,7 @@ export default { [types.SET_ISSUE_MODAL_DATA](state, payload) { state.modal.title = payload.issue.name; - Object.keys(payload.issue).forEach((key) => { + Object.keys(payload.issue).forEach(key => { if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { state.modal.data[key] = { ...state.modal.data[key], diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 4cab2e27a16..5484900276c 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -57,5 +57,4 @@ export default () => ({ }, }, }, - }); diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 123c92aff64..cfa7029b388 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -69,7 +69,8 @@ export default { this.loading = false; } - this.mediator.saveAssignees(this.field) + this.mediator + .saveAssignees(this.field) .then(setLoadingFalse.bind(this)) .catch(() => { setLoadingFalse(); diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 2b8d6207dea..439e8a69df0 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -56,11 +56,7 @@ export default { .update('issue', { confidential }) .then(() => window.location.reload()) .catch(() => { - Flash( - __( - 'Something went wrong trying to change the confidentiality of this issue', - ), - ); + Flash(__('Something went wrong trying to change the confidentiality of this issue')); }); }, }, diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index cdff4105335..48a2b9194aa 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -34,11 +34,7 @@ export default { required: true, type: Object, validator(mediatorObject) { - return ( - mediatorObject.service && - mediatorObject.service.update && - mediatorObject.store - ); + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; }, }, }, @@ -67,8 +63,7 @@ export default { methods: { toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store - .isLockDialogOpen; + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; }, updateLockedAttribute(locked) { @@ -79,9 +74,14 @@ export default { .then(() => window.location.reload()) .catch(() => Flash( - sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), { - issuableDisplayName: this.issuableDisplayName, - }), + sprintf( + __( + 'Something went wrong trying to change the locked state of this %{issuableDisplayName}', + ), + { + issuableDisplayName: this.issuableDisplayName, + }, + ), ), ); }, diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 286a16f7bbf..11b5dbe5f8e 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,78 +1,78 @@ <script> - import { __, n__, sprintf } from '~/locale'; - import tooltip from '~/vue_shared/directives/tooltip'; - import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import { __, n__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + userAvatarImage, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - components: { - userAvatarImage, + participants: { + type: Array, + required: false, + default: () => [], }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - participants: { - type: Array, - required: false, - default: () => [], - }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, - }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, }, - data() { - return { - isShowingMoreParticipants: false, - }; + }, + data() { + return { + isShowingMoreParticipants: false, + }; + }, + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; - }, + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, + }, + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; - }, - onClickCollapsedIcon() { - this.$emit('toggleSidebar'); - }, + onClickCollapsedIcon() { + this.$emit('toggleSidebar'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 5c1ead1a8ac..4ac515e552a 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> - import Store from '../../stores/sidebar_store'; - import participants from './participants.vue'; +import Store from '../../stores/sidebar_store'; +import participants from './participants.vue'; - export default { - components: { - participants, +export default { + components: { + participants, + }, + props: { + mediator: { + type: Object, + required: true, }, - props: { - mediator: { - type: Object, - required: true, - }, - }, - data() { - return { - store: new Store(), - }; - }, - }; + }, + data() { + return { + store: new Store(), + }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 385717e7c1e..95a2c8cce6e 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -21,10 +21,9 @@ export default { }, methods: { onToggleSubscription() { - this.mediator.toggleSubscription() - .catch(() => { - Flash(__('Error occurred when toggling the notification subscription')); - }); + this.mediator.toggleSubscription().catch(() => { + Flash(__('Error occurred when toggling the notification subscription')); + }); }, }, }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 19ec0f05a26..91909cd49b8 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -15,16 +15,22 @@ export default { }, estimateText() { return sprintf( - s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), { + s__( + 'estimateCommand|%{slash_command} will update the estimated time with the latest command.', + ), + { slash_command: '<code>/estimate</code>', - }, false, + }, + false, ); }, spendText() { return sprintf( - s__('spendCommand|%{slash_command} will update the sum of the time spent.'), { + s__('spendCommand|%{slash_command} will update the sum of the time spent.'), + { slash_command: '<code>/spend</code>', - }, false, + }, + false, ); }, }, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index 8660b0546cf..8e8b9f19b6e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -26,7 +26,7 @@ export default { methods: { listenForQuickActions() { $(document).on('ajax:success', '.gfm-form', this.quickActionListened); - eventHub.$on('timeTrackingUpdated', (data) => { + eventHub.$on('timeTrackingUpdated', data => { this.quickActionListened(null, data); }); }, @@ -34,9 +34,7 @@ export default { const subscribedCommands = ['spend_time', 'time_estimate']; let changedCommands; if (data !== undefined) { - changedCommands = data.commands_changes - ? Object.keys(data.commands_changes) - : []; + changedCommands = data.commands_changes ? Object.keys(data.commands_changes) : []; } else { changedCommands = []; } diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index a6b3a674952..bc59774f0a8 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -41,9 +41,9 @@ export default { }, computed: { buttonClasses() { - return this.collapsed ? - 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' : - 'btn btn-default btn-todo issuable-header-btn float-right'; + return this.collapsed + ? 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' + : 'btn btn-default btn-todo issuable-header-btn float-right'; }, buttonLabel() { return this.isTodo ? MARK_TEXT : TODO_TEXT; diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index b267422cd97..225ebb61195 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -37,7 +37,8 @@ class SidebarMoveIssue { // Keep the dropdown open after selecting an option shouldPropagate: false, data: (searchTerm, callback) => { - this.mediator.fetchAutocompleteProjects(searchTerm) + this.mediator + .fetchAutocompleteProjects(searchTerm) .then(callback) .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.')); }, @@ -48,7 +49,7 @@ class SidebarMoveIssue { </a> </li> `, - clicked: (options) => { + clicked: options => { const project = options.selectedObj; const selectedProjectId = options.isMarking ? project.id : 0; this.mediator.setMoveToProjectId(selectedProjectId); @@ -68,17 +69,12 @@ class SidebarMoveIssue { onConfirmClicked() { if (isValidProjectId(this.mediator.store.moveToProjectId)) { - this.$confirmButton - .disable() - .addClass('is-loading'); + this.$confirmButton.disable().addClass('is-loading'); - this.mediator.moveIssue() - .catch(() => { - window.Flash('An error occurred while moving the issue.'); - this.$confirmButton - .enable() - .removeClass('is-loading'); - }); + this.mediator.moveIssue().catch(() => { + window.Flash('An error occurred while moving the issue.'); + this.$confirmButton.enable().removeClass('is-loading'); + }); } } } diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index 87da65a1b1f..1ebdbec7bc9 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -15,15 +15,16 @@ export default class SidebarMilestone { components: { timeTracker, }, - render: createElement => createElement('timeTracker', { - props: { - timeEstimate: parseInt(timeEstimate, 10), - timeSpent: parseInt(timeSpent, 10), - humanTimeEstimate, - humanTimeSpent, - rootPath: '/', - }, - }), + render: createElement => + createElement('timeTracker', { + props: { + timeEstimate: parseInt(timeEstimate, 10), + timeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTimeSpent, + rootPath: '/', + }, + }), }); } } diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 655bf9198b7..6f8214b18ee 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -22,14 +22,15 @@ function mountAssigneesComponent(mediator) { components: { SidebarAssignees, }, - render: createElement => createElement('sidebar-assignees', { - props: { - mediator, - field: el.dataset.field, - signedIn: el.hasAttribute('data-signed-in'), - issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', - }, - }), + render: createElement => + createElement('sidebar-assignees', { + props: { + mediator, + field: el.dataset.field, + signedIn: el.hasAttribute('data-signed-in'), + issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', + }, + }), }); } @@ -83,11 +84,12 @@ function mountParticipantsComponent(mediator) { components: { sidebarParticipants, }, - render: createElement => createElement('sidebar-participants', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-participants', { + props: { + mediator, + }, + }), }); } @@ -102,11 +104,12 @@ function mountSubscriptionsComponent(mediator) { components: { sidebarSubscriptions, }, - render: createElement => createElement('sidebar-subscriptions', { - props: { - mediator, - }, - }), + render: createElement => + createElement('sidebar-subscriptions', { + props: { + mediator, + }, + }), }); } diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 37c97225bfd..cbe20f761ff 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -22,11 +22,15 @@ export default class SidebarService { } update(key, data) { - return Vue.http.put(this.endpoint, { - [key]: data, - }, { - emulateJSON: true, - }); + return Vue.http.put( + this.endpoint, + { + [key]: data, + }, + { + emulateJSON: true, + }, + ); } getProjectsAutocomplete(searchTerm) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 6c87287a4c4..5e7a35e9396 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -2,6 +2,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue'; +import { __ } from '~/locale'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import tooltip from '../../vue_shared/directives/tooltip'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; @@ -31,6 +32,11 @@ export default { required: true, }, }, + deployedTextMap: { + running: __('Deploying to'), + success: __('Deployed to'), + failed: __('Failed to deploy to'), + }, data() { const features = window.gon.features || {}; return { @@ -54,10 +60,13 @@ export default { hasMetrics() { return !!this.deployment.metrics_url; }, + deployedText() { + return this.$options.deployedTextMap[this.deployment.status]; + }, }, methods: { stopEnvironment() { - const msg = 'Are you sure you want to stop this environment?'; + const msg = __('Are you sure you want to stop this environment?'); const isConfirmed = confirm(msg); // eslint-disable-line if (isConfirmed) { @@ -87,10 +96,10 @@ export default { <div class="ci-widget media"> <div class="media-body"> <div class="deploy-body"> - <div class="deployment-info"> + <div class="js-deployment-info deployment-info"> <template v-if="hasDeploymentMeta"> <span> - Deployed to + {{ deployedText }} </span> <tooltip-on-truncate :title="deployment.name" diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js deleted file mode 100644 index a23496c6bf5..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * This file is the centerpiece of an attempt to reduce potential conflicts - * between the CE and EE versions of the MR widget. EE additions to the MR widget should - * be contained in the ee/vue_merge_request_widget directory, and should **extend** - * rather than mutate CE MR Widget code. - * - * This file should be the only source of conflicts between EE and CE. EE-only components should - * imported directly where they are needed, and import paths for EE extensions of CE components - * should overwrite import paths **without** changing the order of dependencies listed here. - */ - -export { default as Vue } from 'vue'; -export { default as SmartInterval } from '~/smart_interval'; -export { default as WidgetHeader } from './components/mr_widget_header.vue'; -export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; -export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; -export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; -export { default as MergedState } from './components/states/mr_widget_merged.vue'; -export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; -export { default as ClosedState } from './components/states/mr_widget_closed.vue'; -export { default as MergingState } from './components/states/mr_widget_merging.vue'; -export { default as WorkInProgressState } from './components/states/work_in_progress.vue'; -export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; -export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; -export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; -export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue'; -export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; -export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue'; -export { default as ShaMismatchState } from './components/states/sha_mismatch.vue'; -export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue'; -export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; -export { default as PipelineFailedState } from './components/states/pipeline_failed.vue'; -export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; -export { default as RebaseState } from './components/states/mr_widget_rebase.vue'; -export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue'; -export { default as CheckingState } from './components/states/mr_widget_checking.vue'; -export { default as MRWidgetStore } from './stores/mr_widget_store'; -export { default as MRWidgetService } from './services/mr_widget_service'; -export { default as eventHub } from './event_hub'; -export { default as getStateKey } from './stores/get_state_key'; -export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue'; -export { default as notify } from '../lib/utils/notify'; -export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; - -export { default as mrWidgetOptions } from './mr_widget_options.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js new file mode 100644 index 00000000000..8780aa4bd1c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js @@ -0,0 +1,3 @@ +import MRWidgetOptions from './mr_widget_options.vue'; + +export default MRWidgetOptions; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index cc6e620f365..60cebbfc2b2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,4 +1,5 @@ -import { Vue, mrWidgetOptions } from './dependencies'; +import Vue from 'vue'; +import MrWidgetOptions from './ee_switch_mr_widget_options'; import Translate from '../vue_shared/translate'; Vue.use(Translate); @@ -6,7 +7,7 @@ Vue.use(Translate); export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; - const vm = new Vue(mrWidgetOptions); + const vm = new Vue(MrWidgetOptions); window.gl.mrWidget = { checkStatus: vm.checkStatus, diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 8180f13a7cb..a5c69d2bc7a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,40 +1,40 @@ <script> +import _ from 'underscore'; +import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; -import { - WidgetHeader, - WidgetMergeHelp, - WidgetPipeline, - Deployment, - WidgetRelatedLinks, - MergedState, - ClosedState, - MergingState, - RebaseState, - WorkInProgressState, - ArchivedState, - ConflictsState, - NothingToMergeState, - MissingBranchState, - NotAllowedState, - ReadyToMergeState, - ShaMismatchState, - UnresolvedDiscussionsState, - PipelineBlockedState, - PipelineFailedState, - FailedToMerge, - MergeWhenPipelineSucceedsState, - AutoMergeFailed, - CheckingState, - MRWidgetStore, - MRWidgetService, - eventHub, - stateMaps, - SquashBeforeMerge, - notify, - SourceBranchRemovalStatus, -} from './dependencies'; +import WidgetHeader from './components/mr_widget_header.vue'; +import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; +import WidgetPipeline from './components/mr_widget_pipeline.vue'; +import Deployment from './components/deployment.vue'; +import WidgetRelatedLinks from './components/mr_widget_related_links.vue'; +import MergedState from './components/states/mr_widget_merged.vue'; +import ClosedState from './components/states/mr_widget_closed.vue'; +import MergingState from './components/states/mr_widget_merging.vue'; +import RebaseState from './components/states/mr_widget_rebase.vue'; +import WorkInProgressState from './components/states/work_in_progress.vue'; +import ArchivedState from './components/states/mr_widget_archived.vue'; +import ConflictsState from './components/states/mr_widget_conflicts.vue'; +import NothingToMergeState from './components/states/nothing_to_merge.vue'; +import MissingBranchState from './components/states/mr_widget_missing_branch.vue'; +import NotAllowedState from './components/states/mr_widget_not_allowed.vue'; +import ReadyToMergeState from './components/states/ready_to_merge.vue'; +import ShaMismatchState from './components/states/sha_mismatch.vue'; +import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue'; +import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue'; +import PipelineFailedState from './components/states/pipeline_failed.vue'; +import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; +import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; +import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; +import CheckingState from './components/states/mr_widget_checking.vue'; +import MRWidgetStore from './stores/ee_switch_mr_widget_store'; +import MRWidgetService from './services/ee_switch_mr_widget_service'; +import eventHub from './event_hub'; +import stateMaps from './stores/ee_switch_state_maps'; +import SquashBeforeMerge from './components/states/squash_before_merge.vue'; +import notify from '~/lib/utils/notify'; +import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import { setFaviconOverlay } from '../lib/utils/common_utils'; @@ -82,6 +82,7 @@ export default { const service = this.createService(store); return { mr: store, + state: store.state, service, }; }, @@ -105,6 +106,17 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState) ); }, + shouldRenderMergedPipeline() { + return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline); + }, + }, + watch: { + state(newVal, oldVal) { + if (newVal !== oldVal && this.shouldRenderMergedPipeline) { + // init polling + this.initPostMergeDeploymentsPolling(); + } + } }, created() { this.initPolling(); @@ -114,11 +126,19 @@ export default { mounted() { this.setFaviconHelper(); this.initDeploymentsPolling(); + + if (this.shouldRenderMergedPipeline) { + this.initPostMergeDeploymentsPolling(); + } }, beforeDestroy() { eventHub.$off('mr.discussion.updated', this.checkStatus); this.pollingInterval.destroy(); this.deploymentsInterval.destroy(); + + if (this.postMergeDeploymentsInterval) { + this.postMergeDeploymentsInterval.destroy(); + } }, methods: { createService(store) { @@ -148,7 +168,13 @@ export default { cb.call(null, data); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); + }, + setFaviconHelper() { + if (this.mr.ciStatusFaviconPath) { + return setFaviconOverlay(this.mr.ciStatusFaviconPath); + } + return Promise.resolve(); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -160,8 +186,14 @@ export default { }); }, initDeploymentsPolling() { - this.deploymentsInterval = new SmartInterval({ - callback: this.fetchDeployments, + this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments); + }, + initPostMergeDeploymentsPolling() { + this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments); + }, + deploymentsPoll(callback) { + return new SmartInterval({ + callback, startingInterval: 30000, maxInterval: 120000, hiddenInterval: 240000, @@ -169,26 +201,29 @@ export default { immediateExecution: true, }); }, - setFaviconHelper() { - if (this.mr.ciStatusFaviconPath) { - return setFaviconOverlay(this.mr.ciStatusFaviconPath); - } - return Promise.resolve(); + fetchDeployments(target) { + return this.service.fetchDeployments(target); }, - fetchDeployments() { - return this.service - .fetchDeployments() - .then(res => res.data) - .then(data => { + fetchPreMergeDeployments() { + return this.fetchDeployments() + .then(({ data }) => { if (data.length) { this.mr.deployments = data; } }) - .catch(() => { - createFlash( - 'Something went wrong while fetching the environments for this merge request. Please try again.', - ); - }); + .catch(() => this.throwDeploymentsError()); + }, + fetchPostMergeDeployments(){ + return this.fetchDeployments('merge_commit') + .then(({ data }) => { + if (data.length) { + this.mr.postMergeDeployments = data; + } + }) + .catch(() => this.throwDeploymentsError()); + }, + throwDeploymentsError() { + createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.')); }, fetchActionsContent() { this.service @@ -201,7 +236,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => createFlash('Something went wrong. Please try again.')); + .catch(() => createFlash(__('Something went wrong. Please try again.'))); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -269,7 +304,8 @@ export default { /> <deployment v-for="deployment in mr.deployments" - :key="deployment.id" + :key="`pre-merge-deploy-${deployment.id}`" + class="js-pre-merge-deploy" :deployment="deployment" /> <div class="mr-section-container"> @@ -310,5 +346,22 @@ export default { <mr-widget-merge-help /> </div> </div> + + <template v-if="shouldRenderMergedPipeline"> + <mr-widget-pipeline + class="js-post-merge-pipeline prepend-top-default" + :pipeline="mr.mergePipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + :source-branch="mr.targetBranch" + :source-branch-link="mr.targetBranch" + /> + <deployment + v-for="postMergeDeployment in mr.postMergeDeployments" + :key="`post-merge-deploy-${postMergeDeployment.id}`" + :deployment="postMergeDeployment" + class="js-post-deployment" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js new file mode 100644 index 00000000000..ea2aabb78fe --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js @@ -0,0 +1,3 @@ +import MRWidgetService from './mr_widget_service'; + +export default MRWidgetService; diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index fecbfec2214..bf5b85b2ae6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -21,8 +21,12 @@ export default class MRWidgetService { return axios.delete(this.endpoints.sourceBranchPath); } - fetchDeployments() { - return axios.get(this.endpoints.ciEnvironmentsStatusPath); + fetchDeployments(targetParam) { + return axios.get(this.endpoints.ciEnvironmentsStatusPath, { + params: { + environment_target: targetParam + } + }); } poll() { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js new file mode 100644 index 00000000000..ebef30e3eab --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js @@ -0,0 +1,3 @@ +import getStateKey from './get_state_key'; + +export default getStateKey; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js new file mode 100644 index 00000000000..92a07c53f2d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js @@ -0,0 +1,3 @@ +import MergeRequestStore from './mr_widget_store'; + +export default MergeRequestStore; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js new file mode 100644 index 00000000000..50cf9503ea7 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js @@ -0,0 +1,3 @@ +import stateMaps from './state_maps'; + +export default stateMaps; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 672e5280b5e..a0c008e7314 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,5 @@ import Timeago from 'timeago.js'; -import { getStateKey } from '../dependencies'; +import getStateKey from './ee_switch_get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; @@ -32,7 +32,9 @@ export default class MergeRequestStore { this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; + this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; + this.postMergeDeployments = this.postMergeDeployments || []; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index a07d63a495d..c78b96695cf 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -1,11 +1,11 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import Icon from '../../icon.vue'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { - 'gl-link': Link, + GlLink, Icon, }, props: { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 807e049caf6..419987d2c50 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -2,14 +2,14 @@ import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import $ from 'jquery'; -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; const { CancelToken } = axios; let axiosSource; export default { components: { - SkeletonLoading, + GlSkeletonLoading, }, props: { content: { @@ -81,7 +81,7 @@ export default { <div ref="markdown-preview" class="md md-previewer"> - <skeleton-loading v-if="isLoading" /> + <gl-skeleton-loading v-if="isLoading" /> <div v-else v-html="previewContent"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index feb7b8f227e..b0a93794013 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,9 +1,9 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; export default { components: { - 'gl-link': Link, + GlLink, }, props: { markdownDocsPath: { diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index 1d9c9220469..f56414c3c63 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,10 +1,10 @@ <script> -import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +import { GlSkeletonLoading } from '@gitlab-org/gitlab-ui'; export default { name: 'SkeletonNote', components: { - SkeletonLoading, + GlSkeletonLoading, }, }; </script> @@ -17,7 +17,7 @@ export default { <div class="timeline-content"> <div class="note-header"></div> <div class="note-body"> - <skeleton-loading /> + <gl-skeleton-loading /> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index 7f1eb6bcec4..11fac3bb12c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,6 +1,11 @@ <script> + import tooltip from '~/vue_shared/directives/tooltip'; + export default { name: 'CollapsedCalendarIcon', + directives: { + tooltip, + }, props: { containerClass: { type: String, @@ -17,6 +22,11 @@ required: false, default: true, }, + tooltipText: { + type: String, + required: false, + default: '', + }, }, methods: { click() { @@ -28,7 +38,13 @@ <template> <div + v-tooltip :class="containerClass" + :title="tooltipText" + data-container="body" + data-placement="left" + data-html="true" + data-boundary="viewport" @click="click" > <i diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index dac438a702d..6e7194ccc9e 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -1,25 +1,23 @@ <script> - import { dateInWords } from '../../../lib/utils/datetime_utility'; - import toggleSidebar from './toggle_sidebar.vue'; + import { __ } from '~/locale'; + import timeagoMixin from '~/vue_shared/mixins/timeago'; + import { dateInWords, timeFor } from '~/lib/utils/datetime_utility'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; export default { name: 'SidebarCollapsedGroupedDatePicker', components: { - toggleSidebar, collapsedCalendarIcon, }, + mixins: [ + timeagoMixin, + ], props: { collapsed: { type: Boolean, required: false, default: true, }, - showToggleSidebar: { - type: Boolean, - required: false, - default: false, - }, minDate: { type: Date, required: false, @@ -51,7 +49,7 @@ }, iconClass() { const disabledClass = this.disableClickableIcons ? 'disabled' : ''; - return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`; + return `sidebar-collapsed-icon calendar-icon ${disabledClass}`; }, }, methods: { @@ -63,7 +61,21 @@ const dateWords = dateInWords(date, true); const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords; - return date ? parsedDateWords : 'None'; + return date ? parsedDateWords : __('None'); + }, + tooltipText(dateType = 'min') { + const defaultText = dateType === 'min' ? __('Start date') : __('Due date'); + const date = this[`${dateType}Date`]; + const timeAgo = dateType === 'min' ? this.timeFormated(date) : timeFor(date); + const dateText = date ? [ + this.dateText(dateType), + `(${timeAgo})`, + ].join(' ') : ''; + + if (date) { + return [defaultText, dateText].join('<br />'); + } + return __('Start and due date'); }, }, }; @@ -71,18 +83,10 @@ <template> <div class="block sidebar-grouped-item"> - <div - v-if="showToggleSidebar" - class="issuable-sidebar-header" - > - <toggle-sidebar - :collapsed="collapsed" - @toggle="toggleSidebar" - /> - </div> <collapsed-calendar-icon v-if="showMinDateBlock" :container-class="iconClass" + :tooltip-text="tooltipText('min')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> @@ -99,7 +103,7 @@ <collapsed-calendar-icon v-if="maxDate" :container-class="iconClass" - :show-icon="!minDate" + :tooltip-text="tooltipText('max')" @click="toggleSidebar" > <span class="sidebar-collapsed-value"> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 14cb44b8619..86c7498a092 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -17,14 +17,14 @@ */ -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import userAvatarImage from './user_avatar_image.vue'; import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', components: { - 'gl-link': Link, + GlLink, userAvatarImage, }, directives: { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1c84baf68ed..c030d75f5a4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -250,6 +250,100 @@ max-width: 100%; } +/* +* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs) +*/ +@mixin build-trace { + background: $black; + color: $gray-darkest; + white-space: pre; + overflow-x: auto; + font-size: 12px; + border-radius: 0; + border: 0; + padding: $grid-size; + + .bash { + display: block; + } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } +} + +@mixin build-trace-top-bar($height) { + height: $height; + min-height: $height; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; + position: sticky; + position: -webkit-sticky; + top: $header-height; + padding: $grid-size; + + .with-performance-bar & { + top: $header-height + $performance-bar-height; + } +} + +/* +* Mixin that handles the position of the controls placed on the top bar +*/ +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { + display: flex; + font-size: $control-font-size; + justify-content: $flex-direction; + align-items: center; + align-self: baseline; + @if $with-grow { + flex-grow: $flex-grow-size; + } + + svg { + width: 15px; + height: 15px; + display: block; + fill: $gl-text-color; + } + + .controllers-buttons { + color: $gl-text-color; + margin: 0 $grid-size; + + &:last-child { + margin-right: 0; + } + } + + .btn-scroll.animate { + .first-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.3s; + } + + .second-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.2s; + } + + .third-triangle { + animation: blinking-scroll-button 1s ease infinite; + } + + &:disabled { + opacity: 1; + } + } + + .btn-scroll:disabled, + .btn-refresh:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + @mixin build-loader-animation { position: relative; white-space: initial; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 227f49ec595..31b258e56dd 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,35 +50,13 @@ position: relative; } - .build-trace { - background: $black; - color: $gray-darkest; - white-space: pre; - overflow-x: auto; - font-size: 12px; - border-radius: 0; - border: 0; - padding: $grid-size; - - .bash { - display: block; - } - &.build-trace-rounded { - border-radius: $border-radius-base; - } + .build-trace { + @include build-trace(); } .top-bar { - height: 35px; - min-height: 35px; - background: $gray-light; - border: 1px solid $border-color; - color: $gl-text-color; - position: sticky; - position: -webkit-sticky; - top: $header-height; - padding: $grid-size; + @include build-trace-top-bar(35px); &.affix { top: $header-height; @@ -116,49 +94,7 @@ } .controllers { - display: flex; - justify-content: center; - align-items: center; - - svg { - height: 15px; - display: block; - fill: $gl-text-color; - } - - .controllers-buttons { - color: $gl-text-color; - margin: 0 $grid-size; - - &:last-child { - margin-right: 0; - } - } - - .btn-scroll.animate { - .first-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.3s; - } - - .second-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.2s; - } - - .third-triangle { - animation: blinking-scroll-button 1s ease infinite; - } - - &:disabled { - opacity: 1; - } - } - - .btn-scroll:disabled { - opacity: 0.35; - cursor: not-allowed; - } + @include build-controllers(15px, center, false, 0); } } @@ -183,12 +119,8 @@ } .with-performance-bar .build-page { - .top-bar { + .top-bar.affix { top: $header-height + $performance-bar-height; - - &.affix { - top: $header-height + $performance-bar-height; - } } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eeabcc0c9bb..7f4aa8244ac 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -46,6 +46,8 @@ class ApplicationController < ActionController::Base :git_import_enabled?, :gitlab_project_import_enabled?, :manifest_import_enabled? + DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store".freeze + rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) render "errors/encoding", layout: "errors", status: 500 @@ -244,6 +246,13 @@ class ApplicationController < ActionController::Base headers['X-XSS-Protection'] = '1; mode=block' headers['X-UA-Compatible'] = 'IE=edge' headers['X-Content-Type-Options'] = 'nosniff' + + if current_user + # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security + # concerns due to caching private data. + headers['Cache-Control'] = DEFAULT_GITLAB_CACHE_CONTROL + headers["Pragma"] = "no-cache" # HTTP 1.0 compatibility + end end def validate_user_service_ticket! diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b3777fd2b0f..f644702cbdb 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -86,10 +86,10 @@ module CreatesCommit def new_merge_request_path project_new_merge_request_path( @project_to_commit_into, + merge_request_source_branch: @branch_name, merge_request: { source_project_id: @project_to_commit_into.id, target_project_id: @project.id, - source_branch: @branch_name, target_branch: @start_branch } ) diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 6e17bc212e4..3802aa5f40f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController include MilestoneActions before_action :projects + before_action :groups, only: :index before_action :milestone, only: [:show, :merge_requests, :participants, :labels] def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(@projects) + @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id)) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do @@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController @milestone = DashboardMilestone.build(@projects, params[:title]) render_404 unless @milestone end + + def groups + @groups ||= GroupsFinder.new(current_user, state_all: true).execute + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index a7cee426cf1..b42116b0f36 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(group_projects, group) + @milestone_states = Milestone.states_count(group_projects, [group]) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 93f3eb2be6d..c1dcc463de7 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -7,7 +7,7 @@ module Groups before_action :authorize_admin_pipeline! def show - define_secret_variables + define_ci_variables end def reset_registration_token @@ -19,7 +19,7 @@ module Groups private - def define_secret_variables + def define_ci_variables @variable = Ci::GroupVariable.new(group: group) .present(current_user: current_user) @variables = group.variables.order_key_asc diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index be708835e30..c0aa39d87c6 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -8,6 +8,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422 + rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503 # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) @@ -62,6 +63,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController render plain: exception.message, status: :unprocessable_entity end + def render_503(exception) + render plain: exception.message, status: :service_unavailable + end + def access @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b06a6f3bb0d..308f666394c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,12 +9,25 @@ class Projects::IssuesController < Projects::ApplicationController include IssuesCalendar include SpammableActions - prepend_before_action :authenticate_user!, only: [:new] + def self.authenticate_user_only_actions + %i[new] + end + + def self.issue_except_actions + %i[index calendar new create bulk_update] + end + + def self.set_issuables_index_only_actions + %i[index calendar] + end + + prepend_before_action :authenticate_user!, only: authenticate_user_only_actions before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update] - before_action :set_issuables_index, only: [:index, :calendar] + before_action :issue, except: issue_except_actions + + before_action :set_issuables_index, only: set_issuables_index_only_actions # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 5639402a1e9..bbf662a63c8 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -89,6 +89,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap def build_merge_request params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence + @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 757b03d0b0e..27b83da4f54 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -168,7 +168,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge - return access_denied! unless @merge_request.can_be_merged_by?(current_user) + access_check_result = merge_access_check + + return access_check_result if access_check_result status = merge! @@ -201,9 +203,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def ci_environments_status - environments = @merge_request.environments_for(current_user).map do |environment| - EnvironmentStatus.new(environment, @merge_request) - end + environments = if ci_environments_status_on_merge_result? + EnvironmentStatus.after_merge_request(@merge_request, current_user) + else + EnvironmentStatus.for_merge_request(@merge_request, current_user) + end render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments) end @@ -241,6 +245,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private + def ci_environments_status_on_merge_result? + params[:environment_target] == 'merge_commit' + end + def target_branch_missing? @merge_request.has_no_commits? && !@merge_request.target_branch_exists? end @@ -256,6 +264,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo return :failed end + merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params) + + unless merge_service.hooks_validation_pass?(@merge_request) + return :hook_validation_error + end + return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) @@ -318,6 +332,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo access_denied! unless access_check end + def merge_access_check + access_denied! unless @merge_request.can_be_merged_by?(current_user) + end + def whitelist_query_limiting # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438') diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 3a1344651df..75e590f3f33 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -68,7 +68,7 @@ module Projects def define_variables define_runners_variables - define_secret_variables + define_ci_variables define_triggers_variables define_badges_variables define_auto_devops_variables @@ -90,7 +90,7 @@ module Projects @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id) end - def define_secret_variables + def define_ci_variables @variable = ::Ci::Variable.new(project: project) .present(current_user: current_user) @variables = project.variables.order_key_asc diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 5beea92689f..81fd3b7a547 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -3,7 +3,7 @@ class PersonalAccessTokensFinder attr_accessor :params - delegate :build, :find, :find_by, to: :execute + delegate :build, :find, :find_by, :find_by_token, to: :execute def initialize(params = {}) @params = params diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 9ece8b0bc5b..57e397f6ca0 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -13,8 +13,8 @@ module CompareHelper def create_mr_path(from = params[:from], to = params[:to], project = @project) project_new_merge_request_path( project, + merge_request_source_branch: to, merge_request: { - source_branch: to, target_branch: from } ) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 23d7aa427bb..8f549bfce73 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -11,10 +11,10 @@ module MergeRequestsHelper def new_mr_from_push_event(event, target_project) { + merge_request_source_branch: event.branch_name, merge_request: { source_project_id: event.project.id, target_project_id: target_project.id, - source_branch: event.branch_name, target_branch: target_project.repository.root_ref } } @@ -51,10 +51,10 @@ module MergeRequestsHelper def mr_change_branches_path(merge_request) project_new_merge_request_path( @project, + merge_request_source_branch: merge_request.source_branch, merge_request: { source_project_id: merge_request.source_project_id, target_project_id: merge_request.target_project_id, - source_branch: merge_request.source_branch, target_branch: merge_request.target_branch }, change_branches: true diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index cdfe8175a42..d73c02ba5d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -593,11 +593,11 @@ module Ci def secret_group_variables return [] unless project.group - project.group.secret_variables_for(ref, project) + project.group.ci_variables_for(ref, project) end def secret_project_variables(environment: persisted_environment) - project.secret_variables_for(ref: ref, environment: environment) + project.ci_variables_for(ref: ref, environment: environment) end def steps diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 95efecfc41d..222e4217e67 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -20,6 +20,12 @@ module Clusters has_many :cluster_projects, class_name: 'Clusters::Project' has_many :projects, through: :cluster_projects, class_name: '::Project' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :groups, through: :cluster_groups, class_name: '::Group' + + has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' + has_one :group, through: :cluster_group, class_name: '::Group' + # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -38,8 +44,12 @@ module Clusters accepts_nested_attributes_for :platform_kubernetes, update_only: true validates :name, cluster_name: true + validates :cluster_type, presence: true validate :restrict_modification, on: :update + validate :no_groups, unless: :group_type? + validate :no_projects, unless: :project_type? + delegate :status, to: :provider, allow_nil: true delegate :status_reason, to: :provider, allow_nil: true delegate :on_creation?, to: :provider, allow_nil: true @@ -50,6 +60,12 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true + enum cluster_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + enum platform_type: { kubernetes: 1 } @@ -122,5 +138,17 @@ module Clusters true end + + def no_groups + if groups.any? + errors.add(:cluster, 'cannot have groups assigned') + end + end + + def no_projects + if projects.any? + errors.add(:cluster, 'cannot have projects assigned') + end + end end end diff --git a/app/models/clusters/group.rb b/app/models/clusters/group.rb new file mode 100644 index 00000000000..2b08a9e47f0 --- /dev/null +++ b/app/models/clusters/group.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Clusters + class Group < ActiveRecord::Base + self.table_name = 'cluster_groups' + + belongs_to :cluster, class_name: 'Clusters::Cluster' + belongs_to :group, class_name: '::Group' + end +end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 344f091c872..7d36f9982ab 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -50,7 +50,8 @@ class CommitStatus < ActiveRecord::Base runner_system_failure: 4, missing_dependency_failure: 5, runner_unsupported: 6, - stale_schedule: 7 + stale_schedule: 7, + job_execution_timeout: 8 } ## diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 91052013592..e57a3383544 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -42,6 +42,7 @@ module DeploymentPlatform { name: 'kubernetes-template', projects: [self], + cluster_type: :project_type, provider_type: :user, platform_type: :kubernetes, platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2aa52bbaeea..a808f9ad4ad 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -9,6 +9,7 @@ module Issuable extend ActiveSupport::Concern include Gitlab::SQL::Pattern + include Redactable include CacheMarkdownField include Participable include Mentionable @@ -32,6 +33,8 @@ module Issuable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description, issuable_state_filter_enabled: true + redact_field :description + belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb new file mode 100644 index 00000000000..5ad96d6cc46 --- /dev/null +++ b/app/models/concerns/redactable.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# This module searches and redacts sensitive information in +# redactable fields. Currently only unsubscribe link is redacted. +# Add following lines into your model: +# +# include Redactable +# redact_field :foo +# +module Redactable + extend ActiveSupport::Concern + + UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe} + + class_methods do + def redact_field(field) + before_validation do + redact_field!(field) if attribute_changed?(field) + end + end + end + + private + + def redact_field!(field) + text = public_send(field) # rubocop:disable GitlabSecurity/PublicSend + return unless text.present? + + redacted = text.gsub(UNSUBSCRIBE_PATTERN, '/sent_notifications/REDACTED/unsubscribe') + + public_send("#{field}=", redacted) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index 522b65e4205..66db4bd92de 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -5,57 +5,50 @@ module TokenAuthenticatable private - def write_new_token(token_field) - new_token = generate_available_token(token_field) - write_attribute(token_field, new_token) - end - - def generate_available_token(token_field) - loop do - token = generate_token(token_field) - break token unless self.class.unscoped.find_by(token_field => token) - end - end - - def generate_token(token_field) - Devise.friendly_token - end - class_methods do - def authentication_token_fields - @token_fields || [] - end - private # rubocop:disable Lint/UselessAccessModifier - def add_authentication_token_field(token_field) + def add_authentication_token_field(token_field, options = {}) @token_fields = [] unless @token_fields + + if @token_fields.include?(token_field) + raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field") + end + @token_fields << token_field + attr_accessor :cleartext_tokens + + strategy = if options[:digest] + TokenAuthenticatableStrategies::Digest.new(self, token_field, options) + else + TokenAuthenticatableStrategies::Insecure.new(self, token_field, options) + end + define_singleton_method("find_by_#{token_field}") do |token| - find_by(token_field => token) if token + strategy.find_token_authenticatable(token) end - define_method("ensure_#{token_field}") do - current_token = read_attribute(token_field) - current_token.blank? ? write_new_token(token_field) : current_token + define_method(token_field) do + strategy.get_token(self) end define_method("set_#{token_field}") do |token| - write_attribute(token_field, token) if token + strategy.set_token(self, token) + end + + define_method("ensure_#{token_field}") do + strategy.ensure_token(self) end # Returns a token, but only saves when the database is in read & write mode define_method("ensure_#{token_field}!") do - send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend - - read_attribute(token_field) + strategy.ensure_token!(self) end # Resets the token, but only saves when the database is in read & write mode define_method("reset_#{token_field}!") do - write_new_token(token_field) - save! if Gitlab::Database.read_write? + strategy.reset_token!(self) end end end diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb new file mode 100644 index 00000000000..f0f7107d627 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Base + def initialize(klass, token_field, options) + @klass = klass + @token_field = token_field + @options = options + end + + def find_token_authenticatable(instance, unscoped = false) + raise NotImplementedError + end + + def get_token(instance) + raise NotImplementedError + end + + def set_token(instance) + raise NotImplementedError + end + + def ensure_token(instance) + write_new_token(instance) unless token_set?(instance) + end + + # Returns a token, but only saves when the database is in read & write mode + def ensure_token!(instance) + reset_token!(instance) unless token_set?(instance) + get_token(instance) + end + + # Resets the token, but only saves when the database is in read & write mode + def reset_token!(instance) + write_new_token(instance) + instance.save! if Gitlab::Database.read_write? + end + + protected + + def write_new_token(instance) + new_token = generate_available_token + set_token(instance, new_token) + end + + def generate_available_token + loop do + token = generate_token + break token unless find_token_authenticatable(token, true) + end + end + + def generate_token + @options[:token_generator] ? @options[:token_generator].call : Devise.friendly_token + end + + def relation(unscoped) + unscoped ? @klass.unscoped : @klass + end + + def token_set?(instance) + raise NotImplementedError + end + + def token_field_name + @token_field + end + end +end diff --git a/app/models/concerns/token_authenticatable_strategies/digest.rb b/app/models/concerns/token_authenticatable_strategies/digest.rb new file mode 100644 index 00000000000..9926662ed66 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/digest.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Digest < Base + def find_token_authenticatable(token, unscoped = false) + return unless token + + token_authenticatable = relation(unscoped).find_by(token_field_name => Gitlab::CryptoHelper.sha256(token)) + + if @options[:fallback] + token_authenticatable ||= fallback_strategy.find_token_authenticatable(token) + end + + token_authenticatable + end + + def get_token(instance) + token = instance.cleartext_tokens&.[](@token_field) + token ||= fallback_strategy.get_token(instance) if @options[:fallback] + + token + end + + def set_token(instance, token) + return unless token + + instance.cleartext_tokens ||= {} + instance.cleartext_tokens[@token_field] = token + instance[token_field_name] = Gitlab::CryptoHelper.sha256(token) + instance[@token_field] = nil if @options[:fallback] + end + + protected + + def fallback_strategy + @fallback_strategy ||= TokenAuthenticatableStrategies::Insecure.new(@klass, @token_field, @options) + end + + def token_set?(instance) + token_digest = instance.read_attribute(token_field_name) + token_digest ||= instance.read_attribute(@token_field) if @options[:fallback] + + token_digest.present? + end + + def token_field_name + "#{@token_field}_digest" + end + end +end diff --git a/app/models/concerns/token_authenticatable_strategies/insecure.rb b/app/models/concerns/token_authenticatable_strategies/insecure.rb new file mode 100644 index 00000000000..5f915259521 --- /dev/null +++ b/app/models/concerns/token_authenticatable_strategies/insecure.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module TokenAuthenticatableStrategies + class Insecure < Base + def find_token_authenticatable(token, unscoped = false) + relation(unscoped).find_by(@token_field => token) if token + end + + def get_token(instance) + instance.read_attribute(@token_field) + end + + def set_token(instance, token) + instance[@token_field] = token if token + end + + protected + + def token_set?(instance) + instance.read_attribute(@token_field).present? + end + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 62dc0f2cbeb..ee5b96e7454 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base metrics&.merge(deployment_time: created_at.to_i) || {} end + def status + 'success' + end + private def prometheus_adapter diff --git a/app/models/environment.rb b/app/models/environment.rb index 0816c395185..1c31c01eb9f 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -149,7 +149,7 @@ class Environment < ActiveRecord::Base end def has_metrics? - prometheus_adapter&.can_query? && available? && last_deployment.present? + prometheus_adapter&.can_query? && available? end def metrics diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index 5ff3acc0e58..a84871f7253 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -3,21 +3,33 @@ class EnvironmentStatus include Gitlab::Utils::StrongMemoize - attr_reader :environment, :merge_request + attr_reader :environment, :merge_request, :sha delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment delegate :deployed_at, to: :deployment, allow_nil: true + delegate :status, to: :deployment - def initialize(environment, merge_request) + def self.for_merge_request(mr, user) + build_environments_status(mr, user, mr.head_pipeline) + end + + def self.after_merge_request(mr, user) + return [] unless mr.merged? + + build_environments_status(mr, user, mr.merge_pipeline) + end + + def initialize(environment, merge_request, sha) @environment = environment @merge_request = merge_request + @sha = sha end def deployment strong_memoize(:deployment) do - environment.first_deployment_for(merge_request.diff_head_sha) + environment.first_deployment_for(sha) end end @@ -26,10 +38,9 @@ class EnvironmentStatus end def changes - sha = merge_request.diff_head_sha return [] if project.route_map_for(sha).nil? - changed_files.map { |file| build_change(file, sha) }.compact + changed_files.map { |file| build_change(file) }.compact end def changed_files @@ -41,7 +52,7 @@ class EnvironmentStatus PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze - def build_change(file, sha) + def build_change(file) public_path = project.public_path_for_source_path(file.new_path, sha) return if public_path.nil? @@ -53,4 +64,22 @@ class EnvironmentStatus external_url: environment.external_url_for(file.new_path, sha) } end + + def self.build_environments_status(mr, user, pipeline) + return [] unless pipeline.present? + + find_environments(user, pipeline).map do |environment| + EnvironmentStatus.new(environment, mr, pipeline.sha) + end + end + private_class_method :build_environments_status + + def self.find_environments(user, pipeline) + env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id) + + Environment.available.where(id: env_ids).select do |environment| + Ability.allowed?(user, :read_environment, environment) + end + end + private_class_method :find_environments end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index a6cebabe089..085ffd16c6a 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -34,50 +34,6 @@ class GlobalMilestone new(title, child_milestones) end - def self.states_count(projects, group = nil) - legacy_group_milestones_count = legacy_group_milestone_states_count(projects) - group_milestones_count = group_milestones_states_count(group) - - legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count| - legacy_group_milestones_count + group_milestones_count - end - end - - def self.group_milestones_states_count(group) - return STATE_COUNT_HASH unless group - - params = { group_ids: [group.id], state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - grouped_by_state = relation.reorder(nil).group(:state).count - - { - opened: grouped_by_state['active'] || 0, - closed: grouped_by_state['closed'] || 0, - all: grouped_by_state.values.sum - } - end - - # Counts the legacy group milestones which must be grouped by title - def self.legacy_group_milestone_states_count(projects) - return STATE_COUNT_HASH unless projects - - params = { project_ids: projects.map(&:id), state: 'all' } - - relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder - project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count - - opened = count_by_state(project_milestones_by_state_and_title, 'active') - closed = count_by_state(project_milestones_by_state_and_title, 'closed') - all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count - - { - opened: opened, - closed: closed, - all: all - } - end - def self.count_by_state(milestones_by_state_and_title, state) milestones_by_state_and_title.count do |(milestone_state, _), _| milestone_state == state diff --git a/app/models/group.rb b/app/models/group.rb index 612c546ca57..adb9169cfcd 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -41,6 +41,9 @@ class Group < Namespace has_many :boards has_many :badges, class_name: 'GroupBadge' + has_many :cluster_groups, class_name: 'Clusters::Group' + has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster' + has_many :todos accepts_nested_attributes_for :variables, allow_destroy: true @@ -366,7 +369,7 @@ class Group < Namespace } end - def secret_variables_for(ref, project) + def ci_variables_for(ref, project) list_of_ids = [self] + ancestors variables = Ci::GroupVariable.where(group: list_of_ids) variables = variables.unprotected unless project.protected_for?(ref) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6559f94a696..7eef08aa6a3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base head_pipeline&.sha == diff_head_sha ? head_pipeline : nil end + def merge_pipeline + return unless merged? + + target_project.pipeline_for(target_branch, merge_commit_sha) + end + # Pattern used to extract `!123` merge request references from text # # This pattern supports cross-project references. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 892a680f221..3cc8e2c44bb 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -145,7 +145,7 @@ class Milestone < ActiveRecord::Base end def participants - User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq + User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct end def self.sort_by_attribute(method) @@ -170,6 +170,22 @@ class Milestone < ActiveRecord::Base sorted.with_order_id_desc end + def self.states_count(projects, groups = nil) + return STATE_COUNT_HASH unless projects || groups + + counts = Milestone + .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .reorder(nil) + .group(:state) + .count + + { + opened: counts['active'] || 0, + closed: counts['closed'] || 0, + all: counts.values.sum + } + end + ## # Returns the String necessary to reference this Milestone in Markdown. Group # milestones only support name references, and do not support cross-project diff --git a/app/models/note.rb b/app/models/note.rb index e1bd943e8e4..990689a95f5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -10,6 +10,7 @@ class Note < ActiveRecord::Base include Awardable include Importable include FasterCacheKeys + include Redactable include CacheMarkdownField include AfterCommitQueue include ResolvableNote @@ -33,6 +34,8 @@ class Note < ActiveRecord::Base cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true + redact_field :note + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with notes. # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 alias_attribute :last_edited_at, :updated_at diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 207146479c0..73a58f2420e 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base include Expirable include TokenAuthenticatable - add_authentication_token_field :token + add_authentication_token_field :token, digest: true, fallback: true REDIS_EXPIRY_TIME = 3.minutes @@ -33,16 +33,22 @@ class PersonalAccessToken < ActiveRecord::Base def self.redis_getdel(user_id) Gitlab::Redis::SharedState.with do |redis| - token = redis.get(redis_shared_state_key(user_id)) + encrypted_token = redis.get(redis_shared_state_key(user_id)) redis.del(redis_shared_state_key(user_id)) - token + begin + Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token) + rescue => ex + logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}" + encrypted_token + end end end def self.redis_store!(user_id, token) + encrypted_token = Gitlab::CryptoHelper.aes256_gcm_encrypt(token) + Gitlab::Redis::SharedState.with do |redis| - redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME) - token + redis.set(redis_shared_state_key(user_id), encrypted_token, ex: REDIS_EXPIRY_TIME) end end diff --git a/app/models/project.rb b/app/models/project.rb index 382fb4f463a..d593cbb223a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1811,7 +1811,7 @@ class Project < ActiveRecord::Base .first end - def secret_variables_for(ref:, environment: nil) + def ci_variables_for(ref:, environment: nil) # EE would use the environment if protected_for?(ref) variables diff --git a/app/models/snippet.rb b/app/models/snippet.rb index e9533ee7c77..1c5846b4023 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -2,6 +2,7 @@ class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel + include Redactable include CacheMarkdownField include Noteable include Participable @@ -18,6 +19,8 @@ class Snippet < ActiveRecord::Base cache_markdown_field :description cache_markdown_field :content + redact_field :description + # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets. # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10392/diffs#note_28719102 alias_attribute :last_edited_at, :updated_at diff --git a/app/models/user.rb b/app/models/user.rb index ca7fc3b058f..cc2cd1b7723 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,7 +28,7 @@ class User < ActiveRecord::Base ignore_column :email_provider ignore_column :authentication_token - add_authentication_token_field :incoming_email_token + add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token default_value_for :admin, false @@ -463,7 +463,7 @@ class User < ActiveRecord::Base def find_by_personal_access_token(token_string) return unless token_string - PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder + PersonalAccessTokensFinder.new(state: 'active').find_by_token(token_string)&.user # rubocop: disable CodeReuse/Finder end # Returns a user for the given SSH key. @@ -1138,7 +1138,7 @@ class User < ActiveRecord::Base events = Event.select(:project_id) .contributions.where(author_id: self) .where("created_at > ?", Time.now - 1.year) - .uniq + .distinct .reorder(nil) Project.where(id: events) @@ -1464,15 +1464,6 @@ class User < ActiveRecord::Base end end - def generate_token(token_field) - if token_field == :incoming_email_token - # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address. - SecureRandom.hex.to_i(16).to_s(36) - else - super - end - end - def self.unique_internal(scope, username, email_pattern, &block) scope.first || create_unique_internal(scope, username, email_pattern, &block) end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 29eaad759bb..a866e76df5a 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -9,7 +9,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_system_failure: 'There has been a runner system failure, please try again', missing_dependency_failure: 'There has been a missing dependency failure', runner_unsupported: 'Your runner is outdated, please upgrade your runner', - stale_schedule: 'Delayed job could not be executed by some reason, please try again' + stale_schedule: 'Delayed job could not be executed by some reason, please try again', + job_execution_timeout: 'The script exceeded the maximum execution time set for the job' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 3f565b826dd..1db6c9eff36 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -108,16 +108,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated namespace = source_project_namespace branch = source_branch - if source_branch_exists? - namespace = link_to(namespace, project_path(source_project)) - branch = link_to(branch, project_tree_path(source_project, source_branch)) - end + namespace_link = source_branch_exists? ? link_to(namespace, project_path(source_project)) : ERB::Util.html_escape(namespace) + branch_link = source_branch_exists? ? link_to(branch, project_tree_path(source_project, source_branch)) : ERB::Util.html_escape(branch) - if for_fork? - namespace + ":" + branch - else - branch - end + for_fork? ? "#{namespace_link}:#{branch_link}" : branch_link end def closing_issues_links diff --git a/app/serializers/environment_status_entity.rb b/app/serializers/environment_status_entity.rb index 3dfa4f204c9..f87cc894d2f 100644 --- a/app/serializers/environment_status_entity.rb +++ b/app/serializers/environment_status_entity.rb @@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity expose :id expose :name + expose :status expose :url do |es| project_environment_path(es.project, es.environment) diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 9ec24f799ef..f33a1654d5e 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :merge_commit_message expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline + expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)} # Booleans expose :merge_ongoing?, as: :merge_ongoing diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index c6e955800af..cd843b8ffa8 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -9,9 +9,9 @@ module Clusters end def execute(project:, access_token: nil) - raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project) + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) - cluster_params = params.merge(user: current_user, projects: [project]) + cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index ced87a1c37a..80de897e94b 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -24,8 +24,8 @@ class DeleteMergedBranchesService < BaseService # rubocop: disable CodeReuse/ActiveRecord def merge_request_branch_names # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY - source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch) - target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch) + source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) (source_names + target_names).uniq end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 52360f775dc..9cbc9fef529 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -40,7 +40,7 @@ module Labels group_labels_applied_to_merge_requests ]) .reorder(nil) - .uniq + .distinct end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 7c88c9abb41..35a22449e34 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -50,8 +50,8 @@ module MergeRequests end def url_for_new_merge_request(branch_name) - merge_request_params = { source_branch: branch_name } - url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params) + url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name) + { branch_name: branch_name, url: url, new_merge_request: true } end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fb44f809c41..70a67baa01c 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -49,6 +49,11 @@ module MergeRequests end end + # Overridden in EE. + def hooks_validation_pass?(_merge_request) + true + end + private def error_check! diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index b03d14fa3cc..f01872b205e 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -85,7 +85,7 @@ module MergeRequests .where.not(target_project: @project).to_a filter_merge_requests(merge_requests).each do |merge_request| - if merge_request.source_branch == @push.branch_name || @push.force_push? + if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commit_shas @@ -104,6 +104,11 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def branch_and_project_match?(merge_request) + merge_request.source_project == @project && + merge_request.source_branch == @push.branch_name + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index e402801a776..f34305e94fa 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -9,8 +9,8 @@ = render 'ci/variables/variable_row', form_field: 'variables', variable: variable = render 'ci/variables/variable_row', form_field: 'variables' .prepend-top-20 - %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' } - %span.hide.js-secret-variables-save-loading-icon + %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' } + %span.hide.js-ci-variables-save-loading-icon = icon('spinner spin') = _('Save variables') %button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } } diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 647948c7dff..a5e6abdba52 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -3,7 +3,7 @@ - expanded = Rails.env.test? -%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) } +%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = _('Variables') diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 596fc3985b3..b7d69539eb7 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -5,7 +5,7 @@ - else - search_path_url = search_path -%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm +%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm.js-navbar %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index c63ff070f70..95bba47802c 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -19,30 +19,23 @@ #js-pipeline-graph-vue #js-tab-builds.tab-pane - - if pipeline.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - pipeline.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)} + - if pipeline.legacy_stages.present? + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Job ID + %th Name + %th + %th Coverage + %th + = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit - .table-holder.pipeline-holder - %table.table.ci-table.pipeline - %thead - %tr - %th Status - %th Job ID - %th Name - %th - %th Coverage - %th - = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - - if @pipeline.failed_builds.present? #js-tab-failures.build-failures.tab-pane.build-page %table.table.responsive-table.ci-table.responsive-table-sm-rounded diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index ff0ed3ed30d..193d437dad1 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -9,6 +9,14 @@ - if @pipeline.commit.present? = render "projects/pipelines/info", commit: @pipeline.commit - = render "projects/pipelines/with_tabs", pipeline: @pipeline + - if @pipeline.builds.empty? && @pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @pipeline.yaml_errors.split(",").each do |error| + %li= error + You can test your .gitlab-ci.yml in #{link_to "CI Lint", project_ci_lint_path(@project)}. + - else + = render "projects/pipelines/with_tabs", pipeline: @pipeline .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cb45928d9a5..1d876cc4a5d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -61,7 +61,10 @@ %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link{ type: 'button' } - = _('No Assignee') + = _('None') + %li.filter-dropdown-item{ data: { value: 'any' } } + %button.btn.btn-link{ type: 'button' } + = _('Any') %li.divider.droplab-item-ignore - if current_user = render 'shared/issuable/user_dropdown_item', @@ -81,7 +84,7 @@ %li.filter-dropdown-item{ data: { value: 'upcoming' } } %button.btn.btn-link{ type: 'button' } = _('Upcoming') - %li.filter-dropdown-item{ 'data-value' => 'started' } + %li.filter-dropdown-item{ data: { value: 'started' } } %button.btn.btn-link{ type: 'button' } = _('Started') %li.divider.droplab-item-ignore diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 10ffe8dd37f..5295e656ab0 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -24,7 +24,7 @@ .block.milestone .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = icon('clock-o', 'aria-hidden': 'true') - %span.milestone-title + %span.milestone-title.collapse-truncated-title - if issuable.milestone = issuable.milestone.title - else diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 09a594cdb4e..72a1733a2a8 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -29,15 +29,14 @@ class PostReceive def process_project_changes(post_received) changes = [] refs = Set.new + @user = post_received.identify - post_received.changes_refs do |oldrev, newrev, ref| - @user ||= post_received.identify(newrev) - - unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false # rubocop:disable Cop/AvoidReturnFromBlocks - end + unless @user + log("Triggered hook for non-existing user \"#{post_received.identifier}\"") + return false + end + post_received.changes_refs do |oldrev, newrev, ref| if Gitlab::Git.tag_ref?(ref) GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute elsif Gitlab::Git.branch_ref?(ref) diff --git a/changelogs/unreleased/34758-create-group-clusters.yml b/changelogs/unreleased/34758-create-group-clusters.yml new file mode 100644 index 00000000000..50efde3cac3 --- /dev/null +++ b/changelogs/unreleased/34758-create-group-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Adds model and migrations to enable group level clusters +merge_request: 22307 +author: +type: other diff --git a/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml new file mode 100644 index 00000000000..d58d8da3a0e --- /dev/null +++ b/changelogs/unreleased/42790-improve-feedback-for-internal-git-access-checks-timeouts.yml @@ -0,0 +1,5 @@ +--- +title: Adds trace of each access check when git push times out +merge_request: 22265 +author: +type: added diff --git a/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml new file mode 100644 index 00000000000..6a305099dde --- /dev/null +++ b/changelogs/unreleased/45068-no-longer-require-a-deploy-to-start-prometheus-monitoring.yml @@ -0,0 +1,5 @@ +--- +title: No longer require a deploy to start Prometheus monitoring +merge_request: 22401 +author: +type: changed diff --git a/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml new file mode 100644 index 00000000000..97052d01b24 --- /dev/null +++ b/changelogs/unreleased/45669-table-in-jobs-on-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Hide all tables on Pipeline when no Jobs for the Pipeline +merge_request: 18540 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml new file mode 100644 index 00000000000..a042debc28f --- /dev/null +++ b/changelogs/unreleased/51335-fail-early-when-user-cannot-be-identified.yml @@ -0,0 +1,5 @@ +--- +title: If user was not found, service hooks won't run on post receive background job +merge_request: 22519 +author: +type: fixed diff --git a/changelogs/unreleased/51527-xss-in-mr-source-branch.yml b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml new file mode 100644 index 00000000000..dae277b6413 --- /dev/null +++ b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml @@ -0,0 +1,5 @@ +--- +title: Fix XSS in merge request source branch name +merge_request: +author: +type: security diff --git a/changelogs/unreleased/52122-fix-broken-whitespace-button.yml b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml new file mode 100644 index 00000000000..3f261eb2ac5 --- /dev/null +++ b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken "Show whitespace changes" button on MRs. +merge_request: 22539 +author: +type: fixed diff --git a/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml new file mode 100644 index 00000000000..adf153f33ce --- /dev/null +++ b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None/Any option for assignee_id in search bar +merge_request: 22599 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml new file mode 100644 index 00000000000..7586d7995b7 --- /dev/null +++ b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Cache pipeline status per SHA. +merge_request: 22589 +author: +type: fixed diff --git a/changelogs/unreleased/53155-structured-logs-params-array.yml b/changelogs/unreleased/53155-structured-logs-params-array.yml new file mode 100644 index 00000000000..4d4f68a5c84 --- /dev/null +++ b/changelogs/unreleased/53155-structured-logs-params-array.yml @@ -0,0 +1,5 @@ +--- +title: Use key-value pair arrays for API query parameter logging instead of hashes +merge_request: 22623 +author: +type: other diff --git a/changelogs/unreleased/53270-remove-mousetrap-rails.yml b/changelogs/unreleased/53270-remove-mousetrap-rails.yml new file mode 100644 index 00000000000..7214c81d73d --- /dev/null +++ b/changelogs/unreleased/53270-remove-mousetrap-rails.yml @@ -0,0 +1,5 @@ +--- +title: Remove mousetrap-rails gem +merge_request: 22647 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/53273-update-moment-to-2-22-2.yml b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml new file mode 100644 index 00000000000..a6b40466927 --- /dev/null +++ b/changelogs/unreleased/53273-update-moment-to-2-22-2.yml @@ -0,0 +1,5 @@ +--- +title: Update moment to 2.22.2 +merge_request: 22648 +author: Takuya Noguchi +type: security diff --git a/changelogs/unreleased/ac-post-merge-pipeline.yml b/changelogs/unreleased/ac-post-merge-pipeline.yml new file mode 100644 index 00000000000..08322c9cb8a --- /dev/null +++ b/changelogs/unreleased/ac-post-merge-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Show post-merge pipeline in merge request page +merge_request: 22292 +author: +type: added diff --git a/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml new file mode 100644 index 00000000000..c8488cbf200 --- /dev/null +++ b/changelogs/unreleased/add-failure-reason-for-execution-timeout.yml @@ -0,0 +1,5 @@ +--- +title: Add failure reason for execution timeout +merge_request: 22224 +author: +type: changed diff --git a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml new file mode 100644 index 00000000000..b89ba754952 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml @@ -0,0 +1,5 @@ +--- +title: Make new merge request URL more friendly when pushing code +merge_request: 22526 +author: "@blackst0ne" +type: changed diff --git a/changelogs/unreleased/fix-53298.yml b/changelogs/unreleased/fix-53298.yml new file mode 100644 index 00000000000..f0bf5470dc8 --- /dev/null +++ b/changelogs/unreleased/fix-53298.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix #53298: JupyterHub restarts should work without errors' +merge_request: 22671 +author: Amit Rathi +type: fixed diff --git a/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml new file mode 100644 index 00000000000..ca3b99e73ab --- /dev/null +++ b/changelogs/unreleased/gt-truncate-milestone-title-on-collapsed-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Truncate milestone title on collapsed sidebar +merge_request: 22624 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml new file mode 100644 index 00000000000..d0c8ed8001d --- /dev/null +++ b/changelogs/unreleased/pl-uprade-prometheus-alertmanager.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Prometheus to 2.4.3 and Alertmanager to 0.15.2 +merge_request: 22600 +author: +type: other diff --git a/changelogs/unreleased/rails5-deprecated-uniq.yml b/changelogs/unreleased/rails5-deprecated-uniq.yml new file mode 100644 index 00000000000..69a169100f0 --- /dev/null +++ b/changelogs/unreleased/rails5-deprecated-uniq.yml @@ -0,0 +1,5 @@ +--- +title: Replace deprecated uniq on a Relation with distinct +merge_request: 22625 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml new file mode 100644 index 00000000000..211d51a3d43 --- /dev/null +++ b/changelogs/unreleased/ravlen-rename-secret-variables-in-codebase.yml @@ -0,0 +1,5 @@ +--- +title: "Secret Variables renamed to CI Variables in the codebase, to match UX" +merge_request: 22414 +author: Marcel Amirault @ravlen +type: changed
\ No newline at end of file diff --git a/changelogs/unreleased/redact-links-dev.yml b/changelogs/unreleased/redact-links-dev.yml new file mode 100644 index 00000000000..338e7965465 --- /dev/null +++ b/changelogs/unreleased/redact-links-dev.yml @@ -0,0 +1,5 @@ +--- +title: Redact personal tokens in unsubscribe links. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/rz_fix_milestone_count.yml b/changelogs/unreleased/rz_fix_milestone_count.yml new file mode 100644 index 00000000000..1013b88e0bc --- /dev/null +++ b/changelogs/unreleased/rz_fix_milestone_count.yml @@ -0,0 +1,5 @@ +--- +title: Fixing count on Milestones +merge_request: 21446 +author: +type: fixed diff --git a/changelogs/unreleased/security-2717-fix-issue-title-xss.yml b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml new file mode 100644 index 00000000000..f2e638e5ab5 --- /dev/null +++ b/changelogs/unreleased/security-2717-fix-issue-title-xss.yml @@ -0,0 +1,5 @@ +--- +title: Escape entity title while autocomplete template rendering to prevent XSS +merge_request: 2556 +author: +type: security diff --git a/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml new file mode 100644 index 00000000000..4cebe814148 --- /dev/null +++ b/changelogs/unreleased/security-51113-hash_personal_access_tokens.yml @@ -0,0 +1,5 @@ +--- +title: Persist only SHA digest of PersonalAccessToken#token +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-hipchat-ssrf.yml b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml new file mode 100644 index 00000000000..cdc95a34fcf --- /dev/null +++ b/changelogs/unreleased/sh-fix-hipchat-ssrf.yml @@ -0,0 +1,5 @@ +--- +title: Prevent SSRF attacks in HipChat integration +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-issue-53153.yml b/changelogs/unreleased/sh-fix-issue-53153.yml new file mode 100644 index 00000000000..ee51631f959 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-53153.yml @@ -0,0 +1,5 @@ +--- +title: Fix extra merge request versions created from forked merge requests +merge_request: 22611 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml new file mode 100644 index 00000000000..ac6ab7cc3f4 --- /dev/null +++ b/changelogs/unreleased/sh-fix-wiki-security-issue-53072.yml @@ -0,0 +1,5 @@ +--- +title: Validate Wiki attachments are valid temporary files +merge_request: +author: +type: security diff --git a/config/initializers/hipchat_client_patch.rb b/config/initializers/hipchat_client_patch.rb new file mode 100644 index 00000000000..aec265312bb --- /dev/null +++ b/config/initializers/hipchat_client_patch.rb @@ -0,0 +1,14 @@ +# This monkey patches the HTTParty used in https://github.com/hipchat/hipchat-rb. +module HipChat + class Client + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end + + class Room + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end + + class User + connection_adapter ::Gitlab::ProxyHTTPConnectionAdapter + end +end diff --git a/config/routes.rb b/config/routes.rb index 8723a928cc3..37c7f98ec98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,7 @@ Rails.application.routes.draw do get 'ide' => 'ide#index' get 'ide/*vueroute' => 'ide#index', format: false + draw :operations draw :instance_statistics end diff --git a/config/routes/project.rb b/config/routes/project.rb index 85872a4122a..73c46f72168 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -149,9 +149,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do scope path: 'merge_requests', controller: 'merge_requests/creations' do post '', action: :create, as: nil - scope path: 'new', as: :new_merge_request do - get '', action: :new - + scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do scope constraints: { format: nil }, action: :new do get :diffs, defaults: { tab: 'diffs' } get :pipelines, defaults: { tab: 'pipelines' } @@ -165,6 +163,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :diff_for_path get :branch_from get :branch_to + get '', action: :new end end diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb new file mode 100644 index 00000000000..203fcfe8eae --- /dev/null +++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column :personal_access_tokens, :token, :string, null: true + + add_column :personal_access_tokens, :token_digest, :string + end + + def down + remove_column :personal_access_tokens, :token_digest + + change_column :personal_access_tokens, :token, :string, null: false + end +end diff --git a/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb new file mode 100644 index 00000000000..4300cd13a45 --- /dev/null +++ b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToTokenDigestOnPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :personal_access_tokens, :token_digest, unique: true + end + + def down + remove_concurrent_index :personal_access_tokens, :token_digest if index_exists?(:personal_access_tokens, :token_digest) + end +end diff --git a/db/migrate/20181014203236_create_cluster_groups.rb b/db/migrate/20181014203236_create_cluster_groups.rb new file mode 100644 index 00000000000..69382d5c851 --- /dev/null +++ b/db/migrate/20181014203236_create_cluster_groups.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateClusterGroups < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :cluster_groups do |t| + t.references :cluster, null: false, foreign_key: { on_delete: :cascade } + t.references :group, null: false, index: true + + t.index [:cluster_id, :group_id], unique: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + end + end +end diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb new file mode 100644 index 00000000000..191e7eb4fb3 --- /dev/null +++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddClusterTypeToClusters < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + PROJECT_CLUSTER_TYPE = 3 + + disable_ddl_transaction! + + def up + add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE) + end + + def down + remove_column(:clusters, :cluster_type) + end +end diff --git a/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb new file mode 100644 index 00000000000..36be819b245 --- /dev/null +++ b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb @@ -0,0 +1,28 @@ +class ScheduleDigestPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + BATCH_SIZE = 10_000 + MIGRATION = 'DigestColumn' + DELAY_INTERVAL = 5.minutes.to_i + + disable_ddl_transaction! + + class PersonalAccessToken < ActiveRecord::Base + include EachBatch + + self.table_name = 'personal_access_tokens' + end + + def up + PersonalAccessToken.where('token is NOT NULL').each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck('MIN(id)', 'MAX(id)').first + BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, ['PersonalAccessToken', :token, :token_digest, *range]) + end + end + + def down + # raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/post_migrate/20181014121030_enqueue_redact_links.rb b/db/post_migrate/20181014121030_enqueue_redact_links.rb new file mode 100644 index 00000000000..1ee4703c88a --- /dev/null +++ b/db/post_migrate/20181014121030_enqueue_redact_links.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class EnqueueRedactLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 1000 + DELAY_INTERVAL = 5.minutes.to_i + MIGRATION = 'RedactLinks' + + disable_ddl_transaction! + + class Note < ActiveRecord::Base + include EachBatch + + self.table_name = 'notes' + self.inheritance_column = :_type_disabled + end + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + self.inheritance_column = :_type_disabled + end + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + self.inheritance_column = :_type_disabled + end + + class Snippet < ActiveRecord::Base + include EachBatch + + self.table_name = 'snippets' + self.inheritance_column = :_type_disabled + end + + def up + disable_statement_timeout do + schedule_migration(Note, 'note') + schedule_migration(Issue, 'description') + schedule_migration(MergeRequest, 'description') + schedule_migration(Snippet, 'description') + end + end + + def down + # nothing to do + end + + private + + def schedule_migration(model, field) + link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%" + + model.where("#{field} like ?", link_pattern).each_batch(of: BATCH_SIZE) do |batch, index| + start_id, stop_id = batch.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, [model.name.demodulize, field, start_id, stop_id]) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7a75aafd7b0..b02b3679e48 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181016152238) do +ActiveRecord::Schema.define(version: 20181017001059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -599,6 +599,14 @@ ActiveRecord::Schema.define(version: 20181016152238) do add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree + create_table "cluster_groups", force: :cascade do |t| + t.integer "cluster_id", null: false + t.integer "group_id", null: false + end + + add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree + add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree + create_table "cluster_platforms_kubernetes", force: :cascade do |t| t.integer "cluster_id", null: false t.datetime_with_timezone "created_at", null: false @@ -654,6 +662,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do t.boolean "enabled", default: true t.string "name", null: false t.string "environment_scope", default: "*", null: false + t.integer "cluster_type", limit: 2, default: 3, null: false end add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree @@ -1539,7 +1548,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do create_table "personal_access_tokens", force: :cascade do |t| t.integer "user_id", null: false - t.string "token", null: false + t.string "token" t.string "name", null: false t.boolean "revoked", default: false t.date "expires_at" @@ -1547,9 +1556,11 @@ ActiveRecord::Schema.define(version: 20181016152238) do t.datetime "updated_at", null: false t.string "scopes", default: "--- []\n", null: false t.boolean "impersonation", default: false, null: false + t.string "token_digest" end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree + add_index "personal_access_tokens", ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree create_table "programming_languages", force: :cascade do |t| @@ -2372,6 +2383,8 @@ ActiveRecord::Schema.define(version: 20181016152238) do add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade + add_foreign_key "cluster_groups", "clusters", on_delete: :cascade + add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "projects", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 03371226041..20fcd2e1724 100644 --- a/doc/README.md +++ b/doc/README.md @@ -165,6 +165,7 @@ configuration. Then customize everything from buildpacks to CI/CD. - [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications) - [Protected variables](ci/variables/README.md#protected-variables) - [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) +- [Executable Runbooks](user/project/clusters/runbooks/index.md) ### Monitor diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 96803746637..481eb692674 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -61,7 +61,7 @@ on an Linux NFS server, do the following: 2. Restart the NFS server process. For example, on CentOS run `service nfs restart`. -## AWS Elastic File System +## Avoid using AWS's Elastic File System (EFS) GitLab strongly recommends against using AWS Elastic File System (EFS). Our support team will not be able to assist on performance issues related to @@ -78,6 +78,23 @@ stored on a local volume. For more details on another person's experience with EFS, see [Amazon's Elastic File System: Burst Credits](https://rawkode.com/2017/04/16/amazons-elastic-file-system-burst-credits/) +## Avoid using PostgreSQL with NFS + +GitLab strongly recommends against running your PostgreSQL database +across NFS. The GitLab support team will not be able to assist on performance issues related to +this configuration. + +Additionally, this configuration is specifically warned against in the +[Postgres Documentation](https://www.postgresql.org/docs/current/static/creating-cluster.html#CREATING-CLUSTER-NFS): + +>PostgreSQL does nothing special for NFS file systems, meaning it assumes NFS behaves exactly like +>locally-connected drives. If the client or server NFS implementation does not provide standard file +>system semantics, this can cause reliability problems. Specifically, delayed (asynchronous) writes +>to the NFS server can cause data corruption problems. + +For supported database architecture, please see our documentation on +[Configuring a Database for GitLab HA](https://docs.gitlab.com/ee/administration/high_availability/database.html). + ## NFS Client mount options Below is an example of an NFS mount point defined in `/etc/fstab` we use on diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index b5d1ff698c6..dcee57def74 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -15,7 +15,7 @@ Omnibus GitLab packages. > **Notes:** > - Redis requires authentication for High Availability. See -> [Redis Security](http://redis.io/topics/security) documentation for more +> [Redis Security](https://redis.io/topics/security) documentation for more > information. We recommend using a combination of a Redis password and tight > firewall rules to secure your Redis service. > - You are highly encouraged to read the [Redis Sentinel][sentinel] documentation @@ -82,7 +82,7 @@ When a **Master** fails to respond, it's the application's responsibility for a new **Master**). To get a better understanding on how to correctly set up Sentinel, please read -the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as +the [Redis Sentinel documentation](https://redis.io/topics/sentinel) first, as failing to configure it correctly can lead to data loss or can bring your whole cluster down, invalidating the failover effort. @@ -885,8 +885,8 @@ Read more on High Availability: [reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure [gh-531]: https://github.com/redis/redis-rb/issues/531 [gh-534]: https://github.com/redis/redis-rb/issues/534 -[redis]: http://redis.io/ -[sentinel]: http://redis.io/topics/sentinel +[redis]: https://redis.io/ +[sentinel]: https://redis.io/topics/sentinel [omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb [source]: ../../install/installation.md [ce]: https://about.gitlab.com/downloads diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md index 5823c575251..2101d36d2b6 100644 --- a/doc/administration/high_availability/redis_source.md +++ b/doc/administration/high_availability/redis_source.md @@ -107,7 +107,7 @@ starting with `sentinel` prefix. Assuming that the Redis Sentinel is installed on the same instance as Redis master with IP `10.0.0.1` (some settings might overlap with the master): -1. [Install Redis Sentinel](http://redis.io/topics/sentinel) +1. [Install Redis Sentinel](https://redis.io/topics/sentinel) 1. Edit `/etc/redis/sentinel.conf`: ```conf @@ -363,7 +363,7 @@ production: port: 26379 # point to sentinel, not to redis port ``` -When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel). +When in doubt, please read [Redis Sentinel documentation](https://redis.io/topics/sentinel). [gh-531]: https://github.com/redis/redis-rb/issues/531 [downloads]: https://about.gitlab.com/downloads diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 0e41a38b7e2..038e043281c 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -84,7 +84,7 @@ Introduced in GitLab 10.0, this file lives in It helps you see requests made directly to the API. For example: ```json -{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"} +{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30} ``` This entry above shows an access to an internal endpoint to check whether an diff --git a/doc/api/tags.md b/doc/api/tags.md index f2a3f9f28d2..826900ca518 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -134,7 +134,7 @@ Parameters: "description": "Amazing release. Wow" }, "name": "v1.0.0", - "target: "2695effb5807a22ff3d138d593fd856244e155e7", + "target": "2695effb5807a22ff3d138d593fd856244e155e7", "message": null } ``` diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md index 0f79f7d1b17..bc948dc6ea9 100644 --- a/doc/ci/examples/container_scanning.md +++ b/doc/ci/examples/container_scanning.md @@ -63,4 +63,10 @@ are still maintained, they have been deprecated with GitLab 11.0 and may be remo in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change. +CAUTION: **Caution:** +Starting with GitLab 11.5, Container Scanning feature is licensed under the name `container_scanning`. +While the old name `sast_container` is still maintained, it has been deprecated with GitLab 11.5 and +may be removed in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` +configuration to reflect that change if you are using the `$GITLAB_FEATURES` environment variable. + [ee]: https://about.gitlab.com/pricing/ diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md index b2c73caae2e..c0346d78141 100644 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md @@ -398,10 +398,10 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams - [Using Docker images documentation][using-docker] - [Example project: Hello GitLab CI/CD on GitLab][hello-gitlab] -[phoenix-site]: http://phoenixframework.org/ "Phoenix Framework" +[phoenix-site]: https://phoenixframework.org/ "Phoenix Framework" [phoenix-learning-guide]: https://hexdocs.pm/phoenix/learning.html "Phoenix Learning Guide" -[phoenix-install]: http://www.phoenixframework.org/docs/installation "Phoenix Installation" -[phoenix-mysql]: http://www.phoenixframework.org/docs/using-mysql "Phoenix with MySQL" +[phoenix-install]: https://hexdocs.pm/phoenix/installation.html "Phoenix Installation" +[phoenix-mysql]: https://hexdocs.pm/phoenix/ecto.html#using-mysql "Phoenix with MySQL" [elixir-site]: http://elixir-lang.org/ "Elixir" [elixir-mix]: http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html "Introduction to mix" [elixir-docs]: http://elixir-lang.org/getting-started/introduction.html "Elixir Documentation" diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 25c6371f3d7..4f4ff85fe1d 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -1,15 +1,13 @@ # Review apps -We currently have review apps available as a manual job in EE pipelines. Here is -[the first implementation](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6259). - -That said, [the Quality team is working](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665) -on making Review Apps automatically deployed by each pipeline, both in CE and EE. +Review Apps are automatically deployed by each pipeline, both in +[CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22010) and +[EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6665). ## How does it work? -1. On every EE [pipeline][gitlab-pipeline] during the `test` stage, you can - start the [`review` job][review-job] +1. On every [pipeline][gitlab-pipeline] during the `test` stage, the + [`review` job][review-job] is automatically started. 1. The `review` job [triggers a pipeline][cng-pipeline] in the [`CNG-mirror`][cng-mirror] [^1] project 1. The `CNG-mirror` pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`, @@ -39,6 +37,9 @@ on making Review Apps automatically deployed by each pipeline, both in CE and EE review app manually, and is also started by GitLab once a branch is deleted - [TBD] Review apps are cleaned up regularly using a pipeline schedule that runs the [`scripts/review_apps/automated_cleanup.rb`][automated_cleanup.rb] script +- If you're unable to log in using the `root` username and password, the + deployment may have failed. Stop the Review App via the `stop_review` + manual job and then retry the `review` job to redeploy the Review App. [^1]: We use the `CNG-mirror` project so that the `CNG`, (**C**loud **N**ative **G**itLab), project's registry is not overloaded with a lot of transient Docker images. diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index 9546f43eea8..73301394e9f 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL ## Third-party resources - [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth) -- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins-ci.org/display/JENKINS/GitLab+OAuth+Plugin) +- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin) - [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/) - [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/) - [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 4d4832184e2..96e788666a1 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -225,10 +225,11 @@ Auto DevOps at the project level. ### Enabling/disabling Auto DevOps at the project-level -1. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it. +If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it. + 1. Go to your project's **Settings > CI/CD > Auto DevOps**. -1. Check the **Default to Auto DevOps pipeline** checkbox. -1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) +1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable) +1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain) that will be used by Auto DevOps to [deploy your application](#auto-deploy) and choose the [deployment strategy](#deployment-strategy). 1. Click **Save changes** for the changes to take effect. @@ -246,12 +247,6 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects which can be enabled from the console with `Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. -### Disable Auto DevOps at the project level - -1. Go to your project's **Settings > CI/CD > Auto DevOps**. -1. Uncheck the **Default to Auto DevOps pipeline** checkbox. -1. Click **Save changes** for the changes to take effect. - ### Deployment strategy > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index b12e86cb0a9..6326aadcdf2 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -83,6 +83,9 @@ under which this application will be deployed.  1. Once ready, click **Create Kubernetes cluster**. + +NOTE: **Note:** +Do not select `f1-micro` from the **Machine type** dropdown. `f1-micro` machines cannot support a full GitLab installation. After a couple of minutes, the cluster will be created. You can also see its status on your [GCP dashboard](https://console.cloud.google.com/kubernetes). diff --git a/doc/university/README.md b/doc/university/README.md index 5edf92b3b09..f19b1ffd3d9 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -205,7 +205,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project ### 4. External Articles -1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460) +1. [2011 WSJ article by Marc Andreessen - Software is Eating the World](https://www.wsj.com/articles/SB10001424053111903480904576512250915629460) 1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/) 1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/) diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 6ff27e495fb..6e0f71017c6 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -303,7 +303,7 @@ A [tool](https://docs.gitlab.com/ee/integration/external-issue-tracker.html) use ### Jenkins -An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins-ci.org/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html). +An Open Source CI tool written using the Java programming language. [Jenkins](https://jenkins.io/) does the same job as GitLab CI, Bamboo, and Travis CI. It is extremely popular. Related [documentation](https://docs.gitlab.com/ee/integration/jenkins.html). ### Jira diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md new file mode 100644 index 00000000000..3b81e439119 --- /dev/null +++ b/doc/user/project/clusters/runbooks/index.md @@ -0,0 +1,49 @@ +# Runbooks + +Runbooks are a collection of documented procedures that explain how to +carry out a particular process, be it starting, stopping, debugging, +or troubleshooting a particular system. + +## Overview + +Historically, runbooks took the form of a decision tree or a detailed +step-by-step guide depending on the condition or system. + +Modern implementations have introduced the concept of an "executable +runbooks", where along with a well define process, operators can execute +code blocks or database queries against a given environment. + +## Nurtch Executable Runbooks + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45912) in GitLab 11.4. + +The JupyterHub app offered via GitLab’s Kubernetes integration now ships +with Nurtch’s Rubix library, providing a simple way to create DevOps +runbooks. A sample runbook is provided, showcasing common operations. + +**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE) +for an overview of how this is acomplished in GitLab!** + +## Requirements + +To create an executable runbook, you will need: + +1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications. + The simplest way to get started is to add a cluster using [GitLab's GKE integration](https://docs.gitlab.com/ee/user/project/clusters/#adding-and-creating-a-new-gke-cluster-via-gitlab). +1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install + all the other applications. It is installed in its own pod inside the cluster which + can run the helm CLI in a safe environment. +1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based + virtual hosting. It acts as a web proxy for your applications. +1. **JupyterHub** - JupyterHub is a multi-user service for managing notebooks across + a team. Jupyter Notebooks provide a web-based interactive programming environment + used for data analysis, visualization, and machine learning. + +## Nurtch + +Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is +an open-source python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks. +Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified +down to a couple of lines of code. Check the [Nurtch Documentation](http://docs.nurtch.com/en/latest) +for more information. diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index f5ea350a58f..9e2434c02ec 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -176,8 +176,8 @@ Clicking on the current board name in the upper left corner will reveal a menu from where you can create another Issue Board and rename or delete the existing one. -Clicking on the main issue board link will take you to the last board -you visited. +When you're revisiting an issue board in a project or group with multiple boards, +GitLab will automatically load the last board you visited. NOTE: **Note:** The Multiple Issue Boards feature is available for diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index e1eede8bbed..89b9621b8b9 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -212,7 +212,7 @@ security measure, necessary just for big companies, like banks and shoppings sit with financial transactions. Now we have a different picture. [According to Josh Aas](https://letsencrypt.org/2015/10/29/phishing-and-malware.html), Executive Director at [ISRG](https://en.wikipedia.org/wiki/Internet_Security_Research_Group): -> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](http://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._ +> _We’ve since come to realize that HTTPS is important for almost all websites. It’s important for any website that allows people to log in with a password, any website that [tracks its users](https://www.washingtonpost.com/news/the-switch/wp/2013/12/10/nsa-uses-google-cookies-to-pinpoint-targets-for-hacking/) in any way, any website that [doesn’t want its content altered](http://arstechnica.com/tech-policy/2014/09/why-comcasts-javascript-ad-injections-threaten-security-net-neutrality/), and for any site that offers content people might not want others to know they are consuming. We’ve also learned that any site not secured by HTTPS [can be used to attack other sites](https://krebsonsecurity.com/2015/04/dont-be-fodder-for-chinas-great-cannon/)._ Therefore, the reason why certificates are so important is that they encrypt the connection between the **client** (you, me, your visitors) diff --git a/doc/user/search/img/issues_filter_none_any.png b/doc/user/search/img/issues_filter_none_any.png Binary files differnew file mode 100644 index 00000000000..9682fc55315 --- /dev/null +++ b/doc/user/search/img/issues_filter_none_any.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 4f1b96b775c..3f9d07dacaa 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -40,6 +40,16 @@ The same process is valid for merge requests. Navigate to your project's **Merge and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. +### Filtering by **None** / **Any** + +Some filter fields like milestone and assignee, allow you to filter by **None** or **Any**. + + + +Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee. + +Selecting **Any** does the opposite. It returns results that have a non-empty value for that field. + ### Searching for specific terms You can filter issues and merge requests by specific terms included in titles or descriptions. diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 4dd6b19e353..ae40b5f7557 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -65,6 +65,8 @@ module API result rescue Gitlab::GitAccess::UnauthorizedError => e break response_with_status(code: 401, success: false, message: e.message) + rescue Gitlab::GitAccess::TimeoutError => e + break response_with_status(code: 503, success: false, message: e.message) rescue Gitlab::GitAccess::NotFoundError => e break response_with_status(code: 404, success: false, message: e.message) end diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb new file mode 100644 index 00000000000..53b5790bfa2 --- /dev/null +++ b/lib/api/validations/types/safe_file.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# This module overrides the Grape type validator defined in +# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb +module API + module Validations + module Types + class SafeFile < ::Grape::Validations::Types::File + def value_coerced?(value) + super && value[:tempfile].is_a?(Tempfile) + end + end + end + end +end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 6e1d4eb335f..24746f4efc6 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -6,7 +6,7 @@ module API def commit_params(attrs) { file_name: attrs[:file][:filename], - file_content: File.read(attrs[:file][:tempfile]), + file_content: attrs[:file][:tempfile].read, branch_name: attrs[:branch] } end @@ -100,7 +100,7 @@ module API success Entities::WikiAttachment end params do - requires :file, type: File, desc: 'The attachment file to be uploaded' + requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index d2029a141e7..6eb5f9e2300 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -151,17 +151,15 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def personal_access_token_check(password) return unless password.present? - token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) + token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password) if token && valid_scoped_token?(token, available_scopes) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) end end - # rubocop: enable CodeReuse/ActiveRecord def valid_oauth_token?(token) token && token.accessible? && valid_scoped_token?(token, [:api]) diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb index 5df6db6f366..c304adc64db 100644 --- a/lib/gitlab/auth/user_auth_finders.rb +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -73,7 +73,6 @@ module Gitlab end end - # rubocop: disable CodeReuse/ActiveRecord def find_personal_access_token token = current_request.params[PRIVATE_TOKEN_PARAM].presence || @@ -82,9 +81,8 @@ module Gitlab return unless token # Expiration, revocation and scopes are verified in `validate_access_token!` - PersonalAccessToken.find_by(token: token) || raise(UnauthorizedError) + PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError) end - # rubocop: enable CodeReuse/ActiveRecord def find_oauth_access_token token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods) diff --git a/lib/gitlab/background_migration/digest_column.rb b/lib/gitlab/background_migration/digest_column.rb new file mode 100644 index 00000000000..22a3bb8f8f3 --- /dev/null +++ b/lib/gitlab/background_migration/digest_column.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# rubocop:disable Style/Documentation +module Gitlab + module BackgroundMigration + class DigestColumn + class PersonalAccessToken < ActiveRecord::Base + self.table_name = 'personal_access_tokens' + end + + def perform(model, attribute_from, attribute_to, start_id, stop_id) + model = model.constantize if model.is_a?(String) + + model.transaction do + relation = model.where(id: start_id..stop_id).where.not(attribute_from => nil).lock + + relation.each do |instance| + instance.update_columns(attribute_to => Gitlab::CryptoHelper.sha256(instance.read_attribute(attribute_from)), + attribute_from => nil) + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/redact_links.rb b/lib/gitlab/background_migration/redact_links.rb new file mode 100644 index 00000000000..f5d3bcdd517 --- /dev/null +++ b/lib/gitlab/background_migration/redact_links.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class RedactLinks + module Redactable + extend ActiveSupport::Concern + + def redact_field!(field) + self[field].gsub!(%r{/sent_notifications/\h{32}/unsubscribe}, '/sent_notifications/REDACTED/unsubscribe') + + if self.changed? + self.update_columns(field => self[field], + "#{field}_html" => nil) + end + end + end + + class Note < ActiveRecord::Base + include EachBatch + include Redactable + + self.table_name = 'notes' + self.inheritance_column = :_type_disabled + end + + class Issue < ActiveRecord::Base + include EachBatch + include Redactable + + self.table_name = 'issues' + self.inheritance_column = :_type_disabled + end + + class MergeRequest < ActiveRecord::Base + include EachBatch + include Redactable + + self.table_name = 'merge_requests' + self.inheritance_column = :_type_disabled + end + + class Snippet < ActiveRecord::Base + include EachBatch + include Redactable + + self.table_name = 'snippets' + self.inheritance_column = :_type_disabled + end + + def perform(model_name, field, start_id, stop_id) + link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%" + model = "Gitlab::BackgroundMigration::RedactLinks::#{model_name}".constantize + + model.where("#{field} like ?", link_pattern).where(id: start_id..stop_id).each do |resource| + resource.redact_field!(field) + end + end + end + end +end diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index b369b9e7600..dfbb83f7bb9 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -42,7 +42,7 @@ module Gitlab end def self.cache_key_for_project(project) - "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status" + "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:projects/#{project.id}/pipeline_status/#{project.commit&.sha}" end def self.update_for_pipeline(pipeline) @@ -84,9 +84,7 @@ module Gitlab def load_from_project return unless commit - self.sha = commit.sha - self.status = commit.status - self.ref = project.default_branch + self.sha, self.status, self.ref = commit.sha, commit.status, project.default_branch end # We only cache the status for the HEAD commit of a project @@ -104,6 +102,8 @@ module Gitlab def load_from_cache Gitlab::Redis::Cache.with do |redis| self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref) + + self.status = nil if self.status.empty? end end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 49e7f7e1fd7..074afe9c412 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -18,11 +18,24 @@ module Gitlab lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".' }.freeze - attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name + LOG_MESSAGES = { + push_checks: "Checking if you are allowed to push...", + delete_default_branch_check: "Checking if default branch is being deleted...", + protected_branch_checks: "Checking if you are force pushing to a protected branch...", + protected_branch_push_checks: "Checking if you are allowed to push to the protected branch...", + protected_branch_deletion_checks: "Checking if you are allowed to delete the protected branch...", + tag_checks: "Checking if you are allowed to change existing tags...", + protected_tag_checks: "Checking if you are creating, updating or deleting a protected tag...", + lfs_objects_exist_check: "Scanning repository for blobs stored in LFS and verifying their files have been uploaded to GitLab...", + commits_check_file_paths_validation: "Validating commits' file paths...", + commits_check: "Validating commit contents..." + }.freeze + + attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name, :logger def initialize( change, user_access:, project:, skip_authorization: false, - skip_lfs_integrity_check: false, protocol: + skip_lfs_integrity_check: false, protocol:, logger: ) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @branch_name = Gitlab::Git.branch_name(@ref) @@ -32,6 +45,9 @@ module Gitlab @skip_authorization = skip_authorization @skip_lfs_integrity_check = skip_lfs_integrity_check @protocol = protocol + + @logger = logger + @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") end def exec(skip_commits_check: false) @@ -49,26 +65,32 @@ module Gitlab protected def push_checks - unless can_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] + logger.log_timed(LOG_MESSAGES[__method__]) do + unless can_push? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code] + end end end def branch_checks return unless branch_name - if deletion? && branch_name == project.default_branch - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + logger.log_timed(LOG_MESSAGES[:delete_default_branch_check]) do + if deletion? && branch_name == project.default_branch + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch] + end end protected_branch_checks end def protected_branch_checks - return unless ProtectedBranch.protected?(project, branch_name) + logger.log_timed(LOG_MESSAGES[__method__]) do + return unless ProtectedBranch.protected?(project, branch_name) # rubocop:disable Cop/AvoidReturnFromBlocks - if forced_push? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + if forced_push? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch] + end end if deletion? @@ -79,23 +101,27 @@ module Gitlab end def protected_branch_deletion_checks - unless user_access.can_delete_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] - end + logger.log_timed(LOG_MESSAGES[__method__]) do + unless user_access.can_delete_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch] + end - unless updated_from_web? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + unless updated_from_web? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch] + end end end def protected_branch_push_checks - if matching_merge_request? - unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] - end - else - unless user_access.can_push_to_branch?(branch_name) - raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + logger.log_timed(LOG_MESSAGES[__method__]) do + if matching_merge_request? + unless user_access.can_merge_to_branch?(branch_name) || user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch] + end + else + unless user_access.can_push_to_branch?(branch_name) + raise GitAccess::UnauthorizedError, push_to_protected_branch_rejected_message + end end end end @@ -103,21 +129,25 @@ module Gitlab def tag_checks return unless tag_name - if tag_exists? && user_access.cannot_do_action?(:admin_project) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + logger.log_timed(LOG_MESSAGES[__method__]) do + if tag_exists? && user_access.cannot_do_action?(:admin_project) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags] + end end protected_tag_checks end def protected_tag_checks - return unless ProtectedTag.protected?(project, tag_name) + logger.log_timed(LOG_MESSAGES[__method__]) do + return unless ProtectedTag.protected?(project, tag_name) # rubocop:disable Cop/AvoidReturnFromBlocks - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? - raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update? + raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion? - unless user_access.can_create_tag?(tag_name) - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + unless user_access.can_create_tag?(tag_name) + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag] + end end end @@ -125,14 +155,20 @@ module Gitlab return if deletion? || newrev.nil? return unless should_run_commit_validations? - # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 - ::Gitlab::GitalyClient.allow_n_plus_1_calls do - commits.each do |commit| - commit_check.validate(commit, validations_for_commit(commit)) + logger.log_timed(LOG_MESSAGES[__method__]) do + # n+1: https://gitlab.com/gitlab-org/gitlab-ee/issues/3593 + ::Gitlab::GitalyClient.allow_n_plus_1_calls do + commits.each do |commit| + logger.check_timeout_reached + + commit_check.validate(commit, validations_for_commit(commit)) + end end end - commit_check.validate_file_paths + logger.log_timed(LOG_MESSAGES[:commits_check_file_paths_validation]) do + commit_check.validate_file_paths + end end # Method overwritten in EE to inject custom validations @@ -194,10 +230,12 @@ module Gitlab end def lfs_objects_exist_check - lfs_check = Checks::LfsIntegrity.new(project, newrev) + logger.log_timed(LOG_MESSAGES[__method__]) do + lfs_check = Checks::LfsIntegrity.new(project, newrev, logger.time_left) - if lfs_check.objects_missing? - raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing] + if lfs_check.objects_missing? + raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:lfs_objects_missing] + end end end diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb index fa3dc1808df..1652d5a30a4 100644 --- a/lib/gitlab/checks/lfs_integrity.rb +++ b/lib/gitlab/checks/lfs_integrity.rb @@ -3,9 +3,10 @@ module Gitlab module Checks class LfsIntegrity - def initialize(project, newrev) + def initialize(project, newrev, time_left) @project = project @newrev = newrev + @time_left = time_left end # rubocop: disable CodeReuse/ActiveRecord @@ -13,7 +14,7 @@ module Gitlab return false unless @newrev && @project.lfs_enabled? new_lfs_pointers = Gitlab::Git::LfsChanges.new(@project.repository, @newrev) - .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT) + .new_pointers(object_limit: ::Gitlab::Git::Repository::REV_LIST_COMMIT_LIMIT, dynamic_timeout: @time_left) return false unless new_lfs_pointers.present? diff --git a/lib/gitlab/checks/timed_logger.rb b/lib/gitlab/checks/timed_logger.rb new file mode 100644 index 00000000000..f365e0a43f6 --- /dev/null +++ b/lib/gitlab/checks/timed_logger.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Gitlab + module Checks + class TimedLogger + TimeoutError = Class.new(StandardError) + + attr_reader :start_time, :header, :log, :timeout + + def initialize(start_time: Time.now, log: [], header: "", timeout:) + @start_time = start_time + @timeout = timeout + @header = header + @log = log + end + + # Adds trace of method being tracked with + # the correspondent time it took to run it. + # We make use of the start default argument + # on unit tests related to this method + # + def log_timed(log_message, start = Time.now) + check_timeout_reached + + timed = true + + yield + + append_message(log_message + time_suffix_message(start: start)) + rescue GRPC::DeadlineExceeded, TimeoutError + args = { cancelled: true } + args[:start] = start if timed + + append_message(log_message + time_suffix_message(args)) + + raise TimeoutError + end + + def check_timeout_reached + return unless time_expired? + + raise TimeoutError + end + + def time_left + (start_time + timeout.seconds) - Time.now + end + + def full_message + header + log.join("\n") + end + + # We always want to append in-place on the log + def append_message(message) + log << message + end + + private + + def time_expired? + time_left <= 0 + end + + def time_suffix_message(cancelled: false, start: nil) + return " (#{elapsed_time(start)}ms)" unless cancelled + + if start + " (cancelled after #{elapsed_time(start)}ms)" + else + " (cancelled)" + end + end + + def elapsed_time(start) + to_ms(Time.now - start) + end + + def to_ms(elapsed) + (elapsed.to_f * 1000).round(2) + end + end + end +end diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb index 5d7d9a751d8..ed5a79d9b9b 100644 --- a/lib/gitlab/ci/parsers/test/junit.rb +++ b/lib/gitlab/ci/parsers/test/junit.rb @@ -14,10 +14,10 @@ module Gitlab test_case = create_test_case(test_case) test_suite.add_test_case(test_case) end - rescue REXML::ParseException => e - raise JunitParserError, "XML parsing failed: #{e.message}" - rescue => e - raise JunitParserError, "JUnit parsing failed: #{e.message}" + rescue REXML::ParseException + raise JunitParserError, "XML parsing failed" + rescue + raise JunitParserError, "JUnit parsing failed" end private diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index 50b0d044265..4babc23a495 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -11,7 +11,8 @@ module Gitlab runner_system_failure: 'runner system failure', missing_dependency_failure: 'missing dependency failure', runner_unsupported: 'unsupported runner', - stale_schedule: 'stale schedule' + stale_schedule: 'stale schedule', + job_execution_timeout: 'job execution timeout' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 6fa59e41d20..db48b187e5e 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -210,7 +210,7 @@ container_scanning: refs: - branches variables: - - $GITLAB_FEATURES =~ /\bsast_container\b/ + - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/ except: variables: - $CONTAINER_SCANNING_DISABLED diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index c819bffdfd6..5ed6427072a 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -73,7 +73,7 @@ module Gitlab # re-running the contributed projects query in each union is expensive, so # use IN(project_ids...) instead. It's the intersection of two users so # the list will be (relatively) short - @contributed_project_ids ||= projects.uniq.pluck(:id) + @contributed_project_ids ||= projects.distinct.pluck(:id) authed_projects = Project.where(id: @contributed_project_ids) .with_feature_available_for_user(feature, current_user) .reorder(nil) diff --git a/lib/gitlab/crypto_helper.rb b/lib/gitlab/crypto_helper.rb new file mode 100644 index 00000000000..68d0b5d8f8a --- /dev/null +++ b/lib/gitlab/crypto_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module CryptoHelper + extend self + + AES256_GCM_OPTIONS = { + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated, + iv: Settings.attr_encrypted_db_key_base_truncated[0..11] + }.freeze + + def sha256(value) + salt = Settings.attr_encrypted_db_key_base_truncated + ::Digest::SHA256.base64digest("#{value}#{salt}") + end + + def aes256_gcm_encrypt(value) + encrypted_token = Encryptor.encrypt(AES256_GCM_OPTIONS.merge(value: value)) + Base64.encode64(encrypted_token) + end + + def aes256_gcm_decrypt(value) + return unless value + + encrypted_token = Base64.decode64(value) + Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token)) + end + end +end diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb index f0fab1e76a3..d7148165408 100644 --- a/lib/gitlab/git/lfs_changes.rb +++ b/lib/gitlab/git/lfs_changes.rb @@ -6,8 +6,8 @@ module Gitlab @newrev = newrev end - def new_pointers(object_limit: nil, not_in: nil) - @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in) + def new_pointers(object_limit: nil, not_in: nil, dynamic_timeout: nil) + @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in, dynamic_timeout) end def all_pointers diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 827c04ae035..802fa65dd63 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -9,6 +9,7 @@ module Gitlab UnauthorizedError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) + TimeoutError = Class.new(StandardError) ProjectMovedError = Class.new(NotFoundError) ERROR_MESSAGES = { @@ -26,11 +27,18 @@ module Gitlab cannot_push_to_read_only: "You can't push code to a read-only GitLab instance." }.freeze + INTERNAL_TIMEOUT = 50.seconds.freeze + LOG_HEADER = <<~MESSAGE + Push operation timed out + + Timing information for debugging purposes: + MESSAGE + DOWNLOAD_COMMANDS = %w{git-upload-pack git-upload-archive}.freeze PUSH_COMMANDS = %w{git-receive-pack}.freeze ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS - attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes + attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes, :logger def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil, auth_result_type: nil) @actor = actor @@ -44,6 +52,7 @@ module Gitlab end def check(cmd, changes) + @logger = Checks::TimedLogger.new(timeout: INTERNAL_TIMEOUT, header: LOG_HEADER) @changes = changes check_protocol! @@ -269,14 +278,19 @@ module Gitlab end def check_single_change_access(change, skip_lfs_integrity_check: false) - Checks::ChangeAccess.new( + change_access = Checks::ChangeAccess.new( change, user_access: user_access, project: project, skip_authorization: deploy_key?, skip_lfs_integrity_check: skip_lfs_integrity_check, - protocol: protocol - ).exec + protocol: protocol, + logger: logger + ) + + change_access.exec + rescue Checks::TimedLogger::TimeoutError + raise TimeoutError, logger.full_message end def deploy_key diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index e731e654f3c..cf2329e489d 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -11,8 +11,8 @@ module Gitlab @changes = deserialize_changes(changes) end - def identify(revision) - super(identifier, project, revision) + def identify + super(identifier) end def changes_refs diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb index 1840bf45154..086ce31e678 100644 --- a/lib/gitlab/gitaly_client/blob_service.rb +++ b/lib/gitlab/gitaly_client/blob_service.rb @@ -72,7 +72,7 @@ module Gitlab GitalyClient::BlobsStitcher.new(response) end - def get_new_lfs_pointers(revision, limit, not_in) + def get_new_lfs_pointers(revision, limit, not_in, dynamic_timeout = nil) request = Gitaly::GetNewLFSPointersRequest.new( repository: @gitaly_repo, revision: encode_binary(revision), @@ -85,7 +85,20 @@ module Gitlab request.not_in_refs += not_in end - response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request, timeout: GitalyClient.medium_timeout) + timeout = + if dynamic_timeout + [dynamic_timeout, GitalyClient.medium_timeout].min + else + GitalyClient.medium_timeout + end + + response = GitalyClient.call( + @gitaly_repo.storage_name, + :blob_service, + :get_new_lfs_pointers, + request, + timeout: timeout + ) map_lfs_pointers(response) end diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index 0014ce2689b..41004408dec 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -6,7 +6,7 @@ module Gitlab def call(severity, datetime, _, data) time = data.delete :time - data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params) + data[:params] = process_params(data) attributes = { time: datetime.utc.iso8601(3), @@ -20,6 +20,14 @@ module Gitlab private + def process_params(data) + return [] unless data.has_key?(:params) + + data[:params] + .each_pair + .map { |k, v| { key: k, value: utf8_encode_values(v) } } + end + def utf8_encode_values(data) case data when Hash diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 28a9fe29465..d5f94ad04f1 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true # Detect user based on identifier like -# key-13 or user-36 or last commit +# key-13 or user-36 module Gitlab module Identifier - def identify(identifier, project = nil, newrev = nil) - if identifier.blank? - identify_using_commit(project, newrev) - elsif identifier =~ /\Auser-\d+\Z/ + def identify(identifier) + if identifier =~ /\Auser-\d+\Z/ # git push over http identify_using_user(identifier) elsif identifier =~ /\Akey-\d+\Z/ @@ -16,19 +14,6 @@ module Gitlab end end - # Tries to identify a user based on a commit SHA. - def identify_using_commit(project, ref) - return if project.nil? && ref.nil? - - commit = project.commit(ref) - - return if !commit || !commit.author_email - - identify_with_cache(:email, commit.author_email) do - commit.author - end - end - # Tries to identify a user based on a user identifier (e.g. "user-123"). # rubocop: disable CodeReuse/ActiveRecord def identify_using_user(identifier) diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 7735b736689..86efe8ad114 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -32,6 +32,7 @@ module Gitlab end validate_localhost!(addrs_info) unless allow_localhost + validate_loopback!(addrs_info) unless allow_localhost validate_local_network!(addrs_info) unless allow_local_network validate_link_local!(addrs_info) unless allow_local_network @@ -86,6 +87,12 @@ module Gitlab raise BlockedUrlError, "Requests to localhost are not allowed" end + def validate_loopback!(addrs_info) + return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? } + + raise BlockedUrlError, "Requests to loopback addresses are not allowed" + end + def validate_local_network!(addrs_info) return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? } diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake index 81829668de8..eec024f9bbb 100644 --- a/lib/tasks/tokens.rake +++ b/lib/tasks/tokens.rake @@ -1,4 +1,7 @@ require_relative '../../app/models/concerns/token_authenticatable.rb' +require_relative '../../app/models/concerns/token_authenticatable_strategies/base.rb' +require_relative '../../app/models/concerns/token_authenticatable_strategies/insecure.rb' +require_relative '../../app/models/concerns/token_authenticatable_strategies/digest.rb' namespace :tokens do desc "Reset all GitLab incoming email tokens" @@ -26,13 +29,6 @@ class TmpUser < ActiveRecord::Base self.table_name = 'users' - def reset_incoming_email_token! - write_new_token(:incoming_email_token) - save!(validate: false) - end - - def reset_feed_token! - write_new_token(:feed_token) - save!(validate: false) - end + add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } + add_authentication_token_field :feed_token end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 26270595c6a..23afc6b245b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -648,6 +648,9 @@ msgstr "" msgid "Are you sure you want to reset the health check token?" msgstr "" +msgid "Are you sure you want to stop this environment?" +msgstr "" + msgid "Are you sure?" msgstr "" @@ -2324,6 +2327,12 @@ msgstr "" msgid "DeployTokens|Your new project deploy token has been created." msgstr "" +msgid "Deployed to" +msgstr "" + +msgid "Deploying to" +msgstr "" + msgid "Deprioritize label" msgstr "" @@ -2750,6 +2759,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to deploy to" +msgstr "" + msgid "Failed to load emoji list." msgstr "" @@ -4030,9 +4042,6 @@ msgstr "" msgid "No" msgstr "" -msgid "No Assignee" -msgstr "" - msgid "No Label" msgstr "" @@ -5607,6 +5616,9 @@ msgstr "" msgid "Something went wrong while fetching comments. Please try again." msgstr "" +msgid "Something went wrong while fetching the environments for this merge request. Please try again." +msgstr "" + msgid "Something went wrong while fetching the projects." msgstr "" @@ -5784,6 +5796,12 @@ msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start and due date" +msgstr "" + +msgid "Start date" +msgstr "" + msgid "Start the Runner!" msgstr "" @@ -6996,9 +7014,6 @@ msgstr "" msgid "You can also star a label to make it a priority label." msgstr "" -msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}" -msgstr "" - msgid "You can easily contribute to them by requesting to join these groups." msgstr "" @@ -7020,6 +7035,9 @@ msgstr "" msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas." msgstr "" +msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}." +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" diff --git a/package.json b/package.json index 086617dc265..d418147b92b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@babel/plugin-syntax-import-meta": "^7.0.0", "@babel/preset-env": "^7.1.0", "@gitlab-org/gitlab-svgs": "^1.33.0", - "@gitlab-org/gitlab-ui": "^1.8.0", + "@gitlab-org/gitlab-ui": "^1.9.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.4", @@ -53,7 +53,7 @@ module QA autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :DeployToken, 'qa/factory/resource/deploy_token' autoload :Branch, 'qa/factory/resource/branch' - autoload :SecretVariable, 'qa/factory/resource/secret_variable' + autoload :CiVariable, 'qa/factory/resource/ci_variable' autoload :Runner, 'qa/factory/resource/runner' autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' @@ -99,7 +99,8 @@ module QA module Integration autoload :Github, 'qa/scenario/test/integration/github' - autoload :LDAP, 'qa/scenario/test/integration/ldap' + autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls' + autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls' autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' @@ -182,7 +183,7 @@ module QA autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens' autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches' - autoload :SecretVariables, 'qa/page/project/settings/secret_variables' + autoload :CiVariables, 'qa/page/project/settings/ci_variables' autoload :Runners, 'qa/page/project/settings/runners' autoload :MergeRequest, 'qa/page/project/settings/merge_request' autoload :Members, 'qa/page/project/settings/members' diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb index 703c78daa99..ffa755b9e88 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/factory/repository/push.rb @@ -45,7 +45,7 @@ module QA repository.use_ssh_key(ssh_key) else repository.uri = repository_http_uri - repository.use_default_credentials + repository.use_default_credentials unless user end username = 'GitLab QA' diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/ci_variable.rb index 24ba3408810..a0aefc61f9f 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/ci_variable.rb @@ -1,13 +1,13 @@ module QA module Factory module Resource - class SecretVariable < Factory::Base + class CiVariable < Factory::Base attr_accessor :key, :value attribute :project do Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-secret-variables' - resource.description = 'project for adding secret variable test' + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' end end @@ -17,7 +17,7 @@ module QA Page::Project::Menu.perform(&:click_ci_cd_settings) Page::Project::Settings::CICD.perform do |setting| - setting.expand_secret_variables do |page| + setting.expand_ci_variables do |page| page.fill_variable(key, value) page.save_variables diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 1062f0b2dbb..de18b9cefa6 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -7,35 +7,42 @@ module QA class Show < Page::Base include Page::Component::Issuable::Common - view 'app/views/projects/issues/show.html.haml' do - element :issue_details, '.issue-details' # rubocop:disable QA/ElementWithPattern - element :title, '.title' # rubocop:disable QA/ElementWithPattern - end - view 'app/views/shared/notes/_form.html.haml' do element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern end - view 'app/views/shared/notes/_comment_button.html.haml' do - element :comment_button, '%strong Comment' # rubocop:disable QA/ElementWithPattern + view 'app/assets/javascripts/notes/components/comment_form.vue' do + element :comment_button + element :comment_input end - def issue_title - find('.issue-details .title').text + view 'app/assets/javascripts/notes/components/discussion_filter.vue' do + element :discussion_filter + element :filter_options end # Adds a comment to an issue # attachment option should be an absolute path def comment(text, attachment: nil) - fill_in(with: text, name: 'note[note]') + fill_element :comment_input, text unless attachment.nil? QA::Page::Component::Dropzone.new(self, '.new-note') .attach_file(attachment) end - click_on 'Comment' + click_element :comment_button + end + + def select_comments_only_filter + click_element :discussion_filter + all_elements(:filter_options).last.click + end + + def select_all_activities_filter + click_element :discussion_filter + all_elements(:filter_options).first.click end end end diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index cc5fc370a5a..12c2409a5a7 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -25,9 +25,9 @@ module QA # rubocop:disable Naming/FileName end end - def expand_secret_variables(&block) + def expand_ci_variables(&block) expand_section(:variables_settings) do - Settings::SecretVariables.perform(&block) + Settings::CiVariables.perform(&block) end end diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index 6a87ef472e4..e7a6e4bf628 100644 --- a/qa/qa/page/project/settings/secret_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -2,7 +2,7 @@ module QA module Page module Project module Settings - class SecretVariables < Page::Base + class CiVariables < Page::Base include Common view 'app/views/ci/variables/_variable_row.html.haml' do @@ -12,7 +12,7 @@ module QA end view 'app/views/ci/variables/_index.html.haml' do - element :save_variables, '.js-secret-variables-save-button' # rubocop:disable QA/ElementWithPattern + element :save_variables, '.js-ci-variables-save-button' # rubocop:disable QA/ElementWithPattern element :reveal_values, '.js-secret-value-reveal-button' # rubocop:disable QA/ElementWithPattern end @@ -33,7 +33,7 @@ module QA end def save_variables - find('.js-secret-variables-save-button').click + find('.js-ci-variables-save-button').click end def reveal_variables diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb new file mode 100644 index 00000000000..bbf4c847f33 --- /dev/null +++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class LDAPNoTLS < Test::Instance::All + tags :ldap_no_tls + end + end + end + end +end diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb index 769fa389785..2a767e57bc6 100644 --- a/qa/qa/scenario/test/integration/ldap.rb +++ b/qa/qa/scenario/test/integration/ldap_tls.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA module Scenario module Test module Integration - class LDAP < Test::Instance::All - tags :ldap + class LDAPTLS < Test::Instance::All + tags :ldap_tls end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index eb9e0297287..a397df03bd2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Manage', :orchestrated, :ldap do + context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do describe 'LDAP login' do it 'user logs into GitLab using LDAP credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 45cb5df8252..44071ec3e45 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -22,7 +22,7 @@ module QA end end - context 'Manage', :orchestrated, :ldap, :skip_signup_disabled do + context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do describe 'while LDAP is enabled' do it_behaves_like 'registration and login' end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb new file mode 100644 index 00000000000..24877d937d2 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'filter issue comments activities' do + let(:issue_title) { 'issue title' } + + it 'user filters comments and activites in an issue' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Issue.fabricate! do |issue| + issue.title = issue_title + end + + expect(page).to have_content(issue_title) + + Page::Project::Issue::Show.perform do |show_page| + show_page.select_comments_only_filter + show_page.comment('/confidential') + show_page.comment('My own comment') + + expect(show_page).not_to have_content("made the issue confidential") + expect(show_page).to have_content("My own comment") + + show_page.select_all_activities_filter + + expect(show_page).to have_content("made the issue confidential") + expect(show_page).to have_content("My own comment") + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 46e1005829d..724c48cd125 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -25,6 +25,7 @@ module QA push.file_content = "Test with unicode characters ❤✓€❄" end + Page::Project::Show.perform(&:wait_for_push) merge_request.visit! expect(page).to have_text('to be squashed') diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 9c64a9a3439..b18dee53cbc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Git clone over HTTP', :ldap do + describe 'Git clone over HTTP', :ldap_no_tls do let(:location) do Page::Project::Show.act do choose_repository_clone_http diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb new file mode 100644 index 00000000000..8e4210482a2 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Git push over HTTP', :ldap_no_tls do + it 'user using a personal access token pushes code to the repository' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token + + user = Factory::Resource::User.new.tap do |user| + user.username = Runtime::User.username + user.password = access_token + end + + push = Factory::Repository::ProjectPush.fabricate! do |push| + push.user = user + push.file_name = 'README.md' + push.file_content = '# This is a test project' + push.commit_message = 'Add README.md' + end + + push.project.visit! + Page::Project::Show.perform(&:wait_for_push) + + expect(page).to have_content('README.md') + expect(page).to have_content('This is a test project') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index b9bed39662f..2f63a07e0c3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Git push over HTTP', :ldap do + describe 'Git push over HTTP', :ldap_no_tls do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index 5f42cb00bd3..ac71cf52b6f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -2,7 +2,7 @@ module QA context 'Create' do - describe 'Protected branch support', :ldap do + describe 'Protected branch support', :ldap_no_tls do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 292f24d9c0d..58b272adcf1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -2,24 +2,24 @@ module QA context 'Verify' do - describe 'Secret variable support' do - it 'user adds a secret variable' do + describe 'CI variable support' do + it 'user adds a CI variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' - resource.value = 'some secret variable' + resource.value = 'some CI variable' end Page::Project::Settings::CICD.perform do |settings| - settings.expand_secret_variables do |page| + settings.expand_ci_variables do |page| expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some secret variable') + expect(page).not_to have_field(with: 'some CI variable') page.reveal_variables - expect(page).to have_field(with: 'some secret variable') + expect(page).to have_field(with: 'some CI variable') end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index caf014c89ea..604641e54b8 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 40cae0793dd..c2fce1e7df1 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -22,7 +22,7 @@ module QA # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb index 198856aec3f..b6d798bf504 100644 --- a/qa/spec/scenario/test/integration/ldap_spec.rb +++ b/qa/spec/scenario/test/integration/ldap_spec.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::LDAP do +describe QA::Scenario::Test::Integration::LDAPNoTLS do context '#perform' do it_behaves_like 'a QA scenario class' do - let(:tags) { [:ldap] } + let(:tags) { [:ldap_no_tls] } + end + end +end + +describe QA::Scenario::Test::Integration::LDAPTLS do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:ldap_tls] } end end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index be3fc832008..4e91068ab88 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -792,4 +792,30 @@ describe ApplicationController do end end end + + context 'control headers' do + controller(described_class) do + def index + render json: :ok + end + end + + context 'user not logged in' do + it 'sets the default headers' do + get :index + + expect(response.headers['Cache-Control']).to be_nil + end + end + + context 'user logged in' do + it 'sets the default headers' do + sign_in(user) + + get :index + + expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store' + end + end + end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 56047c0c8d2..278b980b6d8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do end describe "#index" do + render_views + it 'returns group and project milestones to which the user belongs' do get :index, format: :json @@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id]) expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end + + it 'should contain group and project milestones to which the user belongs to' do + get :index + + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") + end end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index d1d08391164..f7a4a4192d6 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -37,7 +37,7 @@ describe Groups::BoardsController do allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) - list_boards + list_boards format: :html expect(response).to render_template :index expect(response.content_type).to eq 'text/html' diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 28f7e4634a5..64b589a6d83 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -331,10 +331,10 @@ describe Projects::BlobController do expect(response).to redirect_to( project_new_merge_request_path( forked_project, + merge_request_source_branch: "fork-test-1", merge_request: { source_project_id: forked_project.id, target_project_id: project.id, - source_branch: "fork-test-1", target_branch: "master" } ) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index dcfd6c05200..7b0459e0325 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { fork_project(project, user, repository: true) } - let!(:environment) { create(:environment, project: forked) } - let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } - let(:admin) { create(:admin) } + let(:forked) { fork_project(project, user, repository: true) } + let(:sha) { forked.commit.sha } + let(:environment) { create(:environment, project: forked) } + let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } + let(:build) { create(:ci_build, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) } let(:merge_request) do - create(:merge_request, source_project: forked, target_project: project) + create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline) end it 'links to the environment on that project' do @@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do expect(json_response.first['url']).to match /#{forked.full_path}/ end + context "when environment_target is 'merge_commit'" do + it 'returns nothing' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + end + + context 'when is merged' do + let(:source_environment) { create(:environment, project: project) } + let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") } + let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) } + let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) } + let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } + + before do + merge_request.update!(merge_commit_sha: merge_commit_sha) + merge_request.mark_as_merged! + end + + it 'returns the enviroment on the source project' do + get_ci_environments_status(environment_target: 'merge_commit') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['url']).to match /#{project.full_path}/ + end + end + end + # we're trying to reduce the overall number of queries for this method. # set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287 it 'keeps queries in check' do @@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do expect(control_count).to be <= 137 end - def get_ci_environments_status - get :ci_environments_status, + def get_ci_environments_status(extra_params = {}) + params = { namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, - id: merge_request.iid, format: 'json' + id: merge_request.iid, + format: 'json' + } + + get :ci_environments_status, params.merge(extra_params) end end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index bcf289f36a9..6420b70a54f 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -5,6 +5,17 @@ shared_examples 'content not cached without revalidation' do end end +shared_examples 'content not cached without revalidation and no-store' do + it 'ensures content will not be cached without revalidation' do + # Fixed in newer versions of ActivePack, it will only output a single `private`. + if Gitlab.rails5? + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store') + else + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store') + end + end +end + describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -177,7 +188,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' @@ -239,7 +250,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -292,7 +303,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -344,7 +355,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -388,7 +399,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -445,7 +456,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' @@ -498,7 +509,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index bbeba8ce8b9..c9f5d0a813e 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -2,13 +2,28 @@ FactoryBot.define do factory :cluster, class: Clusters::Cluster do user name 'test-cluster' + cluster_type :project_type + + trait :instance do + cluster_type { Clusters::Cluster.cluster_types[:instance_type] } + end trait :project do + cluster_type { Clusters::Cluster.cluster_types[:project_type] } + before(:create) do |cluster, evaluator| cluster.projects << create(:project, :repository) end end + trait :group do + cluster_type { Clusters::Cluster.cluster_types[:group_type] } + + before(:create) do |cluster, evalutor| + cluster.groups << create(:group) + end + end + trait :provided_by_user do provider_type :user platform_type :kubernetes diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 615223a2a88..2cdd3f55b50 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do it 'filters by unassigned' do set_filter('assignee') - click_filter_link('No Assignee') + click_filter_link('None') submit_filter page.within('.add-issues-modal') do diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 8989b2051bb..5c6c1c4fd15 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -114,33 +114,6 @@ describe 'Commits' do expect(page).to have_content 'canceled' end end - - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do - visit pipeline_path(pipeline) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - - it 'shows warning' do - stub_ci_pipeline_yaml_file(nil) - visit pipeline_path(pipeline) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' - end - end - - context 'ci builds disabled' do - before do - stub_ci_builds_disabled - stub_ci_pipeline_yaml_file(nil) - visit pipeline_path(pipeline) - end - - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' - end - end - end end context "when logged as reporter" do @@ -182,6 +155,39 @@ describe 'Commits' do end end end + + describe '.gitlab-ci.yml not found warning' do + before do + project.add_reporter(user) + end + + context 'ci builds enabled' do + it 'does not show warning' do + visit pipeline_path(pipeline) + + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_pipeline_yaml_file(nil) + + visit pipeline_path(pipeline) + + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end + end + + context 'ci builds disabled' do + it 'does not show warning' do + stub_ci_builds_disabled + stub_ci_pipeline_yaml_file(nil) + + visit pipeline_path(pipeline) + + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + end + end end context 'viewing commits for a branch' do diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..0a24c5e906a 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -147,10 +147,12 @@ describe 'Dashboard Projects' do end context 'last push widget', :use_clean_rails_memory_store_caching do + let(:ref) { "feature" } + before do event = create(:push_event, project: project, author: user) - create(:push_event_payload, event: event, ref: 'feature', action: :created) + create(:push_event_payload, event: event, ref: ref, action: :created) Users::LastPushEventService.new(user).cache_last_push_event(event) @@ -165,9 +167,9 @@ describe 'Dashboard Projects' do end expect(page).to have_selector('.merge-request-form') - expect(current_path).to eq project_new_merge_request_path(project) + expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref) expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s - expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature' + expect(find('input#merge_request_source_branch', visible: false).value).to eq ref expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master' end end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index e8ca6a6714f..174840794ed 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -95,9 +95,9 @@ describe 'Group milestones' do end it 'counts milestones correctly' do - expect(find('.top-area .active .badge').text).to eq("2") - expect(find('.top-area .closed .badge').text).to eq("2") - expect(find('.top-area .all .badge').text).to eq("4") + expect(find('.top-area .active .badge').text).to eq("3") + expect(find('.top-area .closed .badge').text).to eq("3") + expect(find('.top-area .all .badge').text).to eq("6") end it 'lists legacy group milestones and group milestones' do diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index d011d2545bb..e910fb54d23 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do expect_filtered_search_input_empty end - it 'selects `no assignee`' do - find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + it 'selects `None`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click expect(page).to have_css(js_dropdown_assignee, visible: false) expect_tokens([assignee_token('none')]) expect_filtered_search_input_empty end + + it 'selects `Any`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect_tokens([assignee_token('any')]) + expect_filtered_search_input_empty + end end describe 'selecting from dropdown without Ajax call' do diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 6ac7ccd00f7..1e1dd5691ab 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -118,7 +118,7 @@ describe 'Visual tokens', :js do describe 'selecting static option from dropdown' do before do - find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click + find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click end it 'changes value in visual token' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 08bf9bc7243..593dc6b6690 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -35,6 +35,21 @@ describe 'GFM autocomplete', :js do expect(page).to have_selector('.atwho-container') end + it 'opens autocomplete menu when field starts with text with item escaping HTML characters' do + alert_title = 'This will execute alert<img src=x onerror=alert(2)<img src=x onerror=alert(1)>' + create(:issue, project: project, title: alert_title) + + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('#') + end + + expect(page).to have_selector('.atwho-container') + + page.within '.atwho-container #at-view-issues' do + expect(page.all('li').first.text).to include(alert_title) + end + end + it 'doesnt open autocomplete menu character is prefixed with text' do page.within '.timeline-content-form' do find('#note-body').native.send_keys('testing') diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb index 0ccab5b2fac..a124c99ecc8 100644 --- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb +++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb @@ -9,10 +9,10 @@ describe 'create a merge request, allowing commits from members who can merge to def visit_new_merge_request visit project_new_merge_request_path( source_project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: source_project.id, target_project_id: target_project.id, - source_branch: 'fix', target_branch: 'master' }) end diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index f744d7941f5..a298ead43db 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -3,15 +3,19 @@ require 'rails_helper' describe 'Merge request > User sees deployment widget', :js do describe 'when deployed to an environment' do let(:user) { create(:user) } - let(:project) { merge_request.target_project } - let(:merge_request) { create(:merge_request, :merged) } + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, :merged, source_project: project) } let(:environment) { create(:environment, project: project) } let(:role) { :developer } - let(:sha) { project.commit('master').id } - let!(:deployment) { create(:deployment, environment: environment, sha: sha) } + let(:ref) { merge_request.target_branch } + let(:sha) { project.commit(ref).id } + let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do + merge_request.update!(merge_commit_sha: sha) project.add_user(user, role) sign_in(user) visit project_merge_request_path(project, merge_request) @@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do end context 'with stop action' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline) } let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - let(:deployment) do - create(:deployment, environment: environment, ref: merge_request.target_branch, - sha: sha, deployable: build, on_stop: 'close_app') - end before do + deployment.update!(on_stop: manual.name) wait_for_requests end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index f15129759de..d2003b61b2a 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -20,10 +20,10 @@ describe 'Merge request > User sees merge widget', :js do before do visit project_new_merge_request_path( project, + merge_request_source_branch: 'feature', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'feature', target_branch: 'master' }) end @@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do context 'view merge request' do let!(:environment) { create(:environment, project: project) } + let(:sha) { project.commit(merge_request.source_branch).sha } + let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) } + let(:build) { create(:ci_build, :success, pipeline: pipeline) } let!(:deployment) do create(:deployment, environment: environment, - ref: 'feature', - sha: merge_request.diff_head_sha) + ref: merge_request.source_branch, + deployable: build, + sha: sha) end before do + merge_request.update!(head_pipeline: pipeline) visit project_merge_request_path(project, merge_request) end it 'shows environments link' do wait_for_requests - page.within('.mr-widget-heading') do + page.within('.js-pre-merge-deploy') do expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) end diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb index 92cc73ddf1f..6dfc819fe8a 100644 --- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb +++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb @@ -13,10 +13,10 @@ describe 'Merge request > User sees WIP help message' do it 'shows a specific WIP hint' do visit project_new_merge_request_path( project, + merge_request_source_branch: 'wip', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'wip', target_branch: 'master' }) @@ -32,10 +32,10 @@ describe 'Merge request > User sees WIP help message' do it 'shows the regular WIP message' do visit project_new_merge_request_path( project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: project.id, target_project_id: project.id, - source_branch: 'fix', target_branch: 'master' }) diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index ae41cf90576..147544740dc 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -109,13 +109,13 @@ describe 'Merge request > User selects branches for new MR', :js do end it 'populates source branch button' do - visit project_new_merge_request_path(project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' }) expect(find('.js-source-branch')).to have_content('fix') end it 'allows to change the diff view' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'fix' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' }) click_link 'Changes' @@ -131,7 +131,7 @@ describe 'Merge request > User selects branches for new MR', :js do end it 'does not allow non-existing branches' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' }) expect(page).to have_content('The form contains the following errors') expect(page).to have_content('Source branch "non-exist-source" does not exist') @@ -140,7 +140,7 @@ describe 'Merge request > User selects branches for new MR', :js do context 'when a branch contains commits that both delete and add the same image' do it 'renders the diff successfully' do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' }) + visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' }) click_link "Changes" @@ -165,7 +165,8 @@ describe 'Merge request > User selects branches for new MR', :js do it 'shows pipelines for a new merge request' do visit project_new_merge_request_path( project, - merge_request: { target_branch: 'master', source_branch: 'fix' }) + merge_request_source_branch: 'fix', + merge_request: { target_branch: 'master' }) page.within('.merge-request') do click_link 'Pipelines' diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb index b81478a481f..6e681185e1f 100644 --- a/spec/features/merge_request/user_uses_quick_actions_spec.rb +++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb @@ -144,7 +144,7 @@ describe 'Merge request > User uses quick actions', :js do describe '/target_branch command in merge request' do let(:another_project) { create(:project, :public, :repository) } - let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + let(:new_url_opts) { { merge_request_source_branch: 'feature' } } before do another_project.add_maintainer(user) diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb index ec1153b7f7f..8ecdec491b8 100644 --- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb +++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb @@ -65,7 +65,7 @@ describe 'User squashes a merge request', :js do context 'when squash is enabled on merge request creation' do before do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' }) check 'merge_request[squash]' click_on 'Submit merge request' wait_for_requests @@ -95,7 +95,7 @@ describe 'User squashes a merge request', :js do context 'when squash is not enabled on merge request creation' do before do - visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch }) + visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' }) click_on 'Submit merge request' wait_for_requests end diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 847b5f0860f..6f620dff82b 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -57,7 +57,7 @@ describe 'Projects > Files > User creates a directory', :js do expect(page).to have_content('From new-feature into master') expect(page).to have_content('Add new directory') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature")) end end @@ -80,8 +80,7 @@ describe 'Projects > Files > User creates a directory', :js do click_button('Create directory') fork = user.fork_of(project2.reload) - - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) end end end diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb index d4dda43c823..d9df4b50621 100644 --- a/spec/features/projects/files/user_creates_files_spec.rb +++ b/spec/features/projects/files/user_creates_files_spec.rb @@ -144,7 +144,7 @@ describe 'Projects > Files > User creates files' do fill_in(:branch_name, with: 'new_branch_name', visible: true) click_button('Commit changes') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') @@ -182,7 +182,7 @@ describe 'Projects > Files > User creates files' do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) expect(page).to have_content('New commit message') end end diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 614b11fa5c8..faf11ee9dd8 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -63,7 +63,7 @@ describe 'Projects > Files > User deletes files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) expect(page).to have_content('New commit message') end end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 9eb65ec159c..c6b2aaea906 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -86,7 +86,7 @@ describe 'Projects > Files > User edits files', :js do fill_in(:branch_name, with: 'new_branch_name', visible: true) click_button('Commit changes') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') @@ -155,7 +155,7 @@ describe 'Projects > Files > User edits files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) wait_for_requests @@ -183,7 +183,7 @@ describe 'Projects > Files > User edits files', :js do fork = user.fork_of(project2) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1")) wait_for_requests diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index e3da28d73c3..09feb315465 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -78,7 +78,7 @@ describe 'Projects > Files > User replaces files', :js do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined")) click_link('Changes') diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb index af3fc528a20..92df8303f46 100644 --- a/spec/features/projects/files/user_uploads_files_spec.rb +++ b/spec/features/projects/files/user_uploads_files_spec.rb @@ -36,7 +36,7 @@ describe 'Projects > Files > User uploads files' do click_button('Upload file') expect(page).to have_content('New commit message') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name")) click_link('Changes') find("a[data-action='diffs']", text: 'Changes').click @@ -92,7 +92,7 @@ describe 'Projects > Files > User uploads files' do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined")) find("a[data-action='diffs']", text: 'Changes').click diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb index 69561b4d733..4be31511ceb 100644 --- a/spec/features/projects/merge_request_button_spec.rb +++ b/spec/features/projects/merge_request_button_spec.rb @@ -22,8 +22,8 @@ describe 'Merge Request button' do it 'shows Create merge request button' do href = project_new_merge_request_path(project, - merge_request: { source_branch: 'feature', - target_branch: 'master' }) + merge_request_source_branch: 'feature', + merge_request: { target_branch: 'master' }) visit url @@ -77,8 +77,8 @@ describe 'Merge Request button' do it 'shows Create merge request button' do href = project_new_merge_request_path(forked_project, - merge_request: { source_branch: 'feature', - target_branch: 'master' }) + merge_request_source_branch: 'feature', + merge_request: { target_branch: 'master' }) visit fork_url diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index fb766addb31..0add129dde2 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -322,6 +322,22 @@ describe 'Project' do end end + context 'content is not cached after signing out', :js do + let(:user) { create(:user, project_view: 'activity') } + let(:project) { create(:project, :repository) } + + it 'does not load activity', :js do + project.add_maintainer(user) + sign_in(user) + visit project_path(project) + sign_out(user) + + page.evaluate_script('window.history.back()') + + expect(page).not_to have_selector('.event-item') + end + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index c40977bc4ee..35971d564d5 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -46,6 +46,7 @@ "diff_head_commit_short_id": { "type": ["string", "null"] }, "merge_commit_message": { "type": ["string", "null"] }, "pipeline": { "type": ["object", "null"] }, + "merge_pipeline": { "type": ["object", "null"] }, "work_in_progress": { "type": "boolean" }, "source_branch_exists": { "type": "boolean" }, "mergeable_discussions_state": { "type": "boolean" }, diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index 4f8701bae01..1fc0e206d5e 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -24,7 +24,7 @@ describe('AjaxFormVariableList', () => { mock = new MockAdapter(axios); const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); - saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); errorBox = container.querySelector('.js-ci-variable-error-box'); ajaxVariableList = new AjaxFormVariableList({ container, @@ -44,7 +44,7 @@ describe('AjaxFormVariableList', () => { describe('onSaveClicked', () => { it('shows loading spinner while waiting for the request', done => { - const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); + const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon'); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false); @@ -172,7 +172,7 @@ describe('AjaxFormVariableList', () => { container = document.querySelector('.js-ci-variable-list-section'); const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); - saveButton = ajaxVariableListEl.querySelector('.js-secret-variables-save-button'); + saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button'); errorBox = container.querySelector('.js-ci-variable-error-box'); ajaxVariableList = new AjaxFormVariableList({ container, diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 7237274eb43..d9d7f61785f 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -1 +1,125 @@ -// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import Vue from 'vue'; +import CompareVersionsComponent from '~/diffs/components/compare_versions.vue'; +import store from '~/mr_notes/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +describe('CompareVersions', () => { + let vm; + const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; + + beforeEach(() => { + vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, { + mergeRequestDiffs: diffsMockData, + mergeRequestDiff: diffsMockData[0], + targetBranch, + }).$mount(); + }); + + describe('template', () => { + it('should render Tree List toggle button with correct attribute values', () => { + const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list'); + + expect(treeListBtn).not.toBeNull(); + expect(treeListBtn.dataset.originalTitle).toBe('Toggle file browser'); + expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0); + expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain( + '#hamburger', + ); + }); + + it('should render comparison dropdowns with correct values', () => { + const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown'); + const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown'); + + expect(sourceDropdown).not.toBeNull(); + expect(targetDropdown).not.toBeNull(); + expect(sourceDropdown.querySelector('a span').innerHTML).toContain('latest version'); + expect(targetDropdown.querySelector('a span').innerHTML).toContain(targetBranch.branchName); + }); + + it('should not render comparison dropdowns if no mergeRequestDiffs are specified', () => { + vm.mergeRequestDiffs = []; + + vm.$nextTick(() => { + const sourceDropdown = vm.$el.querySelector('.mr-version-dropdown'); + const targetDropdown = vm.$el.querySelector('.mr-version-compare-dropdown'); + + expect(sourceDropdown).toBeNull(); + expect(targetDropdown).toBeNull(); + }); + }); + + it('should render whitespace toggle button with correct attributes', () => { + const whitespaceBtn = vm.$el.querySelector('.qa-toggle-whitespace'); + const href = vm.toggleWhitespacePath; + + expect(whitespaceBtn).not.toBeNull(); + expect(whitespaceBtn.getAttribute('href')).toEqual(href); + expect(whitespaceBtn.innerHTML).toContain('Hide whitespace changes'); + }); + + it('should render view types buttons with correct values', () => { + const inlineBtn = vm.$el.querySelector('#inline-diff-btn'); + const parallelBtn = vm.$el.querySelector('#parallel-diff-btn'); + + expect(inlineBtn).not.toBeNull(); + expect(parallelBtn).not.toBeNull(); + expect(inlineBtn.dataset.viewType).toEqual('inline'); + expect(parallelBtn.dataset.viewType).toEqual('parallel'); + expect(inlineBtn.innerHTML).toContain('Inline'); + expect(parallelBtn.innerHTML).toContain('Side-by-side'); + }); + }); + + describe('setInlineDiffViewType', () => { + it('should persist the view type in the url', () => { + const viewTypeBtn = vm.$el.querySelector('#inline-diff-btn'); + viewTypeBtn.click(); + + expect(window.location.toString()).toContain('?view=inline'); + }); + }); + + describe('setParallelDiffViewType', () => { + it('should persist the view type in the url', () => { + const viewTypeBtn = vm.$el.querySelector('#parallel-diff-btn'); + viewTypeBtn.click(); + + expect(window.location.toString()).toContain('?view=parallel'); + }); + }); + + describe('comparableDiffs', () => { + it('should not contain the first item in the mergeRequestDiffs property', () => { + const { comparableDiffs } = vm; + const comparableDiffsMock = diffsMockData.slice(1); + + expect(comparableDiffs).toEqual(comparableDiffsMock); + }); + }); + + describe('isWhitespaceVisible', () => { + const originalHref = window.location.href; + + afterEach(() => { + window.history.replaceState({}, null, originalHref); + }); + + it('should return "true" when no "w" flag is present in the URL (default)', () => { + expect(vm.isWhitespaceVisible()).toBe(true); + }); + + it('should return "false" when the flag is set to "1" in the URL', () => { + window.history.replaceState({}, null, '?w=1'); + + expect(vm.isWhitespaceVisible()).toBe(false); + }); + + it('should return "true" when the flag is set to "0" in the URL', () => { + window.history.replaceState({}, null, '?w=0'); + + expect(vm.isWhitespaceVisible()).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js new file mode 100644 index 00000000000..d72ad7818dd --- /dev/null +++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js @@ -0,0 +1,42 @@ +export default [ + { + versionIndex: 4, + createdAt: '2018-10-23T11:49:16.611Z', + commitsCount: 4, + latest: true, + shortCommitSha: 'de7a8f7f', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995', + }, + { + versionIndex: 3, + createdAt: '2018-10-23T11:46:40.617Z', + commitsCount: 3, + latest: false, + shortCommitSha: 'e78fc18f', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e', + }, + { + versionIndex: 2, + createdAt: '2018-10-04T09:57:39.648Z', + commitsCount: 2, + latest: false, + shortCommitSha: '48da7e7e', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e', + }, + { + versionIndex: 1, + createdAt: '2018-09-25T20:30:39.493Z', + commitsCount: 1, + latest: false, + shortCommitSha: '47bac2ed', + versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + comparePath: + '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0', + }, +]; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 4b6d3d5bcba..fed04cbaed8 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -221,6 +221,7 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1); expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1); + expect(state.diffFiles[0].parallelDiffLines[0].right.discussions).toEqual([]); expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1); expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js deleted file mode 100644 index bc973407b25..00000000000 --- a/spec/javascripts/job_spec.js +++ /dev/null @@ -1,265 +0,0 @@ -// import $ from 'jquery'; -// import MockAdapter from 'axios-mock-adapter'; -// import axios from '~/lib/utils/axios_utils'; -// import { numberToHumanSize } from '~/lib/utils/number_utils'; -// import '~/lib/utils/datetime_utility'; -// import Job from '~/job'; -// import '~/breakpoints'; -// import waitForPromises from 'spec/helpers/wait_for_promises'; - -// describe('Job', () => { -// const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; -// let mock; -// let response; -// let job; - -// preloadFixtures('builds/build-with-artifacts.html.raw'); - -// beforeEach(() => { -// loadFixtures('builds/build-with-artifacts.html.raw'); - -// spyOnDependency(Job, 'visitUrl'); - -// response = {}; - -// mock = new MockAdapter(axios); - -// mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); -// }); - -// afterEach(() => { -// mock.restore(); - -// clearTimeout(job.timeout); -// }); - -// describe('class constructor', () => { -// beforeEach(() => { -// jasmine.clock().install(); -// }); - -// afterEach(() => { -// jasmine.clock().uninstall(); -// }); - -// describe('running build', () => { -// it('updates the build trace on an interval', function (done) { -// response = { -// html: '<span>Update<span>', -// status: 'running', -// state: 'newstate', -// append: true, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/Update/); -// expect(job.state).toBe('newstate'); - -// response = { -// html: '<span>More</span>', -// status: 'running', -// state: 'finalstate', -// append: true, -// complete: true, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); -// expect(job.state).toBe('finalstate'); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('replaces the entire build trace', (done) => { -// response = { -// html: '<span>Update<span>', -// status: 'running', -// append: false, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - -// response = { -// html: '<span>Different</span>', -// status: 'running', -// append: false, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); -// expect($('#build-trace .js-build-output').text()).toMatch(/Different/); -// }) -// .then(done) -// .catch(done.fail); -// }); -// }); - -// describe('truncated information', () => { -// describe('when size is less than total', () => { -// it('shows information about truncated log', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('shows the size in KiB', (done) => { -// const size = 50; - -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(size)}`); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('shows incremented size', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// complete: false, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(50)}`); - -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: true, -// size: 10, -// total: 100, -// complete: true, -// }; -// }) -// .then(() => jasmine.clock().tick(4001)) -// .then(waitForPromises) -// .then(() => { -// expect( -// document.querySelector('.js-truncated-info-size').textContent.trim(), -// ).toEqual(`${numberToHumanSize(60)}`); -// }) -// .then(done) -// .catch(done.fail); -// }); - -// it('renders the raw link', () => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// expect( -// document.querySelector('.js-raw-link').textContent.trim(), -// ).toContain('Complete Raw'); -// }); -// }); - -// describe('when size is equal than total', () => { -// it('does not show the trunctated information', (done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 100, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(() => { -// expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); -// }) -// .then(done) -// .catch(done.fail); -// }); -// }); -// }); - -// describe('output trace', () => { -// beforeEach((done) => { -// response = { -// html: '<span>Update</span>', -// status: 'success', -// append: false, -// size: 50, -// total: 100, -// }; - -// job = new Job(); - -// waitForPromises() -// .then(done) -// .catch(done.fail); -// }); - -// it('should render trace controls', () => { -// const controllers = document.querySelector('.controllers'); - -// expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull(); -// expect(controllers.querySelector('.js-scroll-up')).not.toBeNull(); -// expect(controllers.querySelector('.js-scroll-down')).not.toBeNull(); -// }); - -// it('should render received output', () => { -// expect( -// document.querySelector('.js-build-output').innerHTML, -// ).toEqual('<span>Update</span>'); -// }); -// }); -// }); - -// }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index a3477c5f8c6..565b87de248 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -113,6 +113,22 @@ describe('Dashboard', () => { }); }); + it('hides the dropdown list when there is no environments', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { ...propsData, hasMetrics: true, showPanels: false }, + }); + + component.store.storeEnvironmentsData([]); + + setTimeout(() => { + const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul'); + + expect(dropdownMenuEnvironments.length).toEqual(0); + done(); + }); + }); + it('renders the dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index ce850bc621e..d26ffa4b4a6 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -2,54 +2,48 @@ import Vue from 'vue'; import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import { getTimeago } from '~/lib/utils/datetime_utility'; +import mountComponent from '../../helpers/vue_mount_component_helper'; -const deploymentMockData = { - id: 15, - name: 'review/diplo', - url: '/root/acets-review-apps/environments/15', - stop_url: '/root/acets-review-apps/environments/15/stop', - metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', - metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', - external_url: 'http://diplo.', - external_url_formatted: 'diplo.', - deployed_at: '2017-03-22T22:44:42.258Z', - deployed_at_formatted: 'Mar 22, 2017 10:44pm', - changes: [ - { - path: 'index.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', - }, - { - path: 'imgs/gallery.html', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', - }, - { - path: 'about/', - external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', - }, - ], -}; -const createComponent = () => { +describe('Deployment component', () => { const Component = Vue.extend(deploymentComponent); + const deploymentMockData = { + id: 15, + name: 'review/diplo', + url: '/root/review-apps/environments/15', + stop_url: '/root/review-apps/environments/15/stop', + metrics_url: '/root/review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/review-apps/environments/15/metrics', + external_url: 'http://gitlab.com.', + external_url_formatted: 'gitlab', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + }; - return new Component({ - el: document.createElement('div'), - propsData: { deployment: { ...deploymentMockData } }, - }); -}; - -describe('Deployment component', () => { let vm; - beforeEach(() => { - vm = createComponent(); - }); - afterEach(() => { vm.$destroy(); }); - describe('computed', () => { + describe('', () => { + beforeEach(() => { + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); + }); + describe('deployTimeago', () => { it('return formatted date', () => { const readable = getTimeago().format(deploymentMockData.deployed_at); @@ -111,9 +105,7 @@ describe('Deployment component', () => { expect(vm.hasDeploymentMeta).toEqual(false); }); }); - }); - describe('methods', () => { describe('stopEnvironment', () => { const url = '/foo/bar'; const returnPromise = () => @@ -152,42 +144,33 @@ describe('Deployment component', () => { expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); }); }); - }); - - describe('template', () => { - let el; - - beforeEach(() => { - vm = createComponent(deploymentMockData); - el = vm.$el; - }); it('renders deployment name', () => { - expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual( deploymentMockData.url, ); - expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); + expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name); }); it('renders external URL', () => { - expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( + expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual( deploymentMockData.external_url, ); - expect(el.querySelector('.js-deploy-url').innerText).toContain('View app'); + expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app'); }); it('renders stop button', () => { - expect(el.querySelector('.btn')).not.toBeNull(); + expect(vm.$el.querySelector('.btn')).not.toBeNull(); }); it('renders deployment time', () => { - expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); + expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago); }); it('renders metrics component', () => { - expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull(); + expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull(); }); }); @@ -196,8 +179,7 @@ describe('Deployment component', () => { window.gon = window.gon || {}; window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = true; - - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -216,7 +198,7 @@ describe('Deployment component', () => { window.gon.features = window.gon.features || {}; window.gon.features.ciEnvironmentsStatusChanges = false; - vm = createComponent(deploymentMockData); + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); }); afterEach(() => { @@ -228,4 +210,44 @@ describe('Deployment component', () => { expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); }); }); + + describe('deployment status', () => { + describe('running', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'running' }), + }); + }); + + it('renders information about running deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); + }); + }); + + describe('success', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'success' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to'); + }); + }); + + describe('failed', () => { + beforeEach(() => { + vm = mountComponent(Component, { + deployment: Object.assign({}, deploymentMockData, { status: 'failed' }), + }); + }); + + it('renders information about finished deployment', () => { + expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain( + 'Failed to deploy to', + ); + }); + }); + }); }); 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 d1a064b9f4d..27b6c91e154 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => { it('should fetch deployments', done => { spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }])); - vm.fetchDeployments(); + vm.fetchPreMergeDeployments(); setTimeout(() => { expect(vm.service.fetchDeployments).toHaveBeenCalled(); @@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => { deployed_at: '2017-03-22T22:44:42.258Z', deployed_at_formatted: 'Mar 22, 2017 10:44pm', changes, + status: 'success' }; beforeEach(done => { @@ -486,4 +487,189 @@ describe('mrWidgetOptions', () => { ).toEqual(changes.length); }); }); + + describe('pipeline for target branch after merge', () => { + describe('with information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + vm.mr.mergePipeline = { + id: 127, + user: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + active: true, + coverage: null, + source: 'push', + created_at: '2018-10-22T11:41:35.186Z', + updated_at: '2018-10-22T11:41:35.433Z', + path: '/root/ci-web-terminal/pipelines/127', + flags: { + latest: true, + stuck: true, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'test', + title: 'test: pending', + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/root/ci-web-terminal/pipelines/127#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png', + }, + path: '/root/ci-web-terminal/pipelines/127#test', + dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test', + }, + ], + artifacts: [], + manual_actions: [], + scheduled_actions: [], + }, + ref: { + name: 'master', + path: '/root/ci-web-terminal/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'aa1939133d373c94879becb79d91828a892ee319', + short_id: 'aa193913', + title: "Merge branch 'master-test' into 'master'", + created_at: '2018-10-22T11:41:33.000Z', + parent_ids: [ + '4622f4dd792468993003caf2e3be978798cbe096', + '76598df914cdfe87132d0c3c40f80db9fa9396a4', + ], + message: + "Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1", + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2018-10-22T11:41:33.000Z', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2018-10-22T11:41:33.000Z', + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + status_tooltip_html: null, + path: '/root', + }, + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319', + }, + cancel_path: '/root/ci-web-terminal/pipelines/127/cancel', + }; + vm.$nextTick(done); + }); + + it('renders pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull(); + }); + + describe('with post merge deployments', () => { + beforeEach(done => { + vm.mr.postMergeDeployments = [{ + id: 15, + name: 'review/diplo', + url: '/root/acets-review-apps/environments/15', + stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', + metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', + external_url: 'http://diplo.', + external_url_formatted: 'diplo.', + deployed_at: '2017-03-22T22:44:42.258Z', + deployed_at_formatted: 'Mar 22, 2017 10:44pm', + changes: [ + { + path: 'index.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html', + }, + { + path: 'imgs/gallery.html', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html', + }, + { + path: 'about/', + external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/', + }, + ], + status: 'success' + }]; + + vm.$nextTick(done); + }); + + it('renders post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull(); + }); + }); + }); + + describe('without information for target branch pipeline', () => { + beforeEach(done => { + vm.mr.state = 'merged'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + }); + + describe('when state is not merged', () => { + beforeEach(done => { + vm.mr.state = 'archived'; + + vm.$nextTick(done); + }); + + it('does not render pipeline block', () => { + expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull(); + }); + + it('does not render post deployment information', () => { + expect(vm.$el.querySelector('.js-post-deployment')).toBeNull(); + }); + }); + }); }); 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 026a0c7ea09..3483b7d387d 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 @@ -11,16 +11,6 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('should render toggle sidebar if showToggleSidebar', (done) => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); - - vm.showToggleSidebar = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); - done(); - }); - }); - describe('toggleCollapse events', () => { beforeEach((done) => { spyOn(vm, 'toggleSidebar'); @@ -28,12 +18,6 @@ describe('collapsedGroupedDatePicker', () => { Vue.nextTick(done); }); - it('should emit when sidebar is toggled', () => { - vm.$el.querySelector('.gutter-toggle').click(); - - expect(vm.toggleSidebar).toHaveBeenCalled(); - }); - it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); @@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => { expect(icons.length).toEqual(1); expect(icons[0].innerText.trim()).toEqual('None'); }); + + it('should have tooltip as `Start and due date`', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + + expect(icons[0].dataset.originalTitle).toBe('Start and due date'); + }); }); }); diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb new file mode 100644 index 00000000000..3e107ac3027 --- /dev/null +++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + subject { described_class.new } + + describe '#perform' do + context 'token is not yet hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + end + + it 'saves token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01'))) + end + + it 'erases token' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token }.from('token-01').to(nil)) + end + end + + context 'token is already hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01') + end + + it 'does not change existing token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token_digest }) + end + + it 'leaves token empty' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token }.from(nil)) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/redact_links_spec.rb b/spec/lib/gitlab/background_migration/redact_links_spec.rb new file mode 100644 index 00000000000..a40e68069cc --- /dev/null +++ b/spec/lib/gitlab/background_migration/redact_links_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::RedactLinks, :migration, schema: 20181014121030 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:merge_requests) { table(:merge_requests) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + def create_merge_request(id, params) + params.merge!(id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}") + + merge_requests.create(params) + end + + def create_issue(id, params) + params.merge!(id: id, title: "issue#{id}", project_id: project.id) + + issues.create(params) + end + + def create_note(id, params) + params[:id] = id + + notes.create(params) + end + + def create_snippet(id, params) + params.merge!(id: id, author_id: user.id) + + snippets.create(params) + end + + def create_resource(model, id, params) + send("create_#{model.name.underscore}", id, params) + end + + shared_examples_for 'redactable resource' do + it 'updates only matching texts' do + matching_text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + redacted_text = 'some text /sent_notifications/REDACTED/unsubscribe more text' + create_resource(model, 1, { field => matching_text }) + create_resource(model, 2, { field => 'not matching text' }) + create_resource(model, 3, { field => matching_text }) + create_resource(model, 4, { field => redacted_text }) + create_resource(model, 5, { field => matching_text }) + + expected = { field => 'some text /sent_notifications/REDACTED/unsubscribe more text', + "#{field}_html" => nil } + expect_any_instance_of("Gitlab::BackgroundMigration::RedactLinks::#{model}".constantize).to receive(:update_columns).with(expected).and_call_original + + subject.perform(model, field, 2, 4) + + expect(model.where(field => matching_text).pluck(:id)).to eq [1, 5] + expect(model.find(3).reload[field]).to eq redacted_text + end + end + + context 'resource is Issue' do + it_behaves_like 'redactable resource' do + let(:model) { Issue } + let(:field) { :description } + end + end + + context 'resource is Merge Request' do + it_behaves_like 'redactable resource' do + let(:model) { MergeRequest } + let(:field) { :description } + end + end + + context 'resource is Note' do + it_behaves_like 'redactable resource' do + let(:model) { Note } + let(:field) { :note } + end + end + + context 'resource is Snippet' do + it_behaves_like 'redactable resource' do + let(:model) { Snippet } + let(:field) { :description } + end + end +end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 4d5081b0a75..e5999a1c509 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do expect(pipeline_status.status).to eq(status) expect(pipeline_status.ref).to eq(ref) end + + context 'when status is empty string' do + before do + Gitlab::Redis::Cache.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: '', ref: ref }) + end + end + + it 'reads the status as nil' do + pipeline_status.load_from_cache + + expect(pipeline_status.status).to eq(nil) + end + end end describe '#has_cache?' do diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 4df426c54ae..81804ba5c76 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -10,13 +10,16 @@ describe Gitlab::Checks::ChangeAccess do let(:ref) { 'refs/heads/master' } let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } } let(:protocol) { 'ssh' } + let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT } + let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) } subject(:change_access) do described_class.new( changes, project: project, user_access: user_access, - protocol: protocol + protocol: protocol, + logger: logger ) end @@ -30,6 +33,19 @@ describe Gitlab::Checks::ChangeAccess do end end + context 'when time limit was reached' do + it 'raises a TimeoutError' do + logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout) + access = described_class.new(changes, + project: project, + user_access: user_access, + protocol: protocol, + logger: logger) + + expect { access.exec }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) + end + end + context 'when the user is not allowed to push to the repo' do it 'raises an error' do expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index ec22e3a198e..0488720cec8 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Checks::LfsIntegrity do include ProjectForksHelper + let!(:time_left) { 50 } let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:newrev) do @@ -15,7 +16,7 @@ describe Gitlab::Checks::LfsIntegrity do operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects") end - subject { described_class.new(project, newrev) } + subject { described_class.new(project, newrev, time_left) } describe '#objects_missing?' do let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } diff --git a/spec/lib/gitlab/checks/timed_logger_spec.rb b/spec/lib/gitlab/checks/timed_logger_spec.rb new file mode 100644 index 00000000000..0ed3940c038 --- /dev/null +++ b/spec/lib/gitlab/checks/timed_logger_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Checks::TimedLogger do + let!(:timeout) { 50.seconds } + let!(:start) { Time.now } + let!(:ref) { "bar" } + let!(:logger) { described_class.new(start_time: start, timeout: timeout) } + let!(:log_messages) do + { + foo: "Foo message..." + } + end + + before do + logger.append_message("Checking ref: #{ref}") + end + + describe '#log_timed' do + it 'logs message' do + Timecop.freeze(start + 30.seconds) do + logger.log_timed(log_messages[:foo], start) { bar_check } + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (30000.0ms)") + end + + context 'when time limit was reached' do + it 'cancels action' do + Timecop.freeze(start + 50.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + bar_check + end + end.to raise_error(described_class::TimeoutError) + end + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled)") + end + + it 'cancels action with time elapsed if work was performed' do + Timecop.freeze(start + 30.seconds) do + expect do + logger.log_timed(log_messages[:foo], start) do + grpc_check + end + end.to raise_error(described_class::TimeoutError) + + expect(logger.full_message).to eq("Checking ref: bar\nFoo message... (cancelled after 30000.0ms)") + end + end + end + end + + def bar_check + 2 + 2 + end + + def grpc_check + raise GRPC::DeadlineExceeded + end +end diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index 2ce858836e3..854c4cb718c 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -54,7 +54,7 @@ describe Gitlab::Ci::Build::Policy::Variables do expect(policy).not_to be_satisfied_by(pipeline, seed) end - it 'allows to evaluate regular secret variables' do + it 'allows to evaluate regular CI variables' do create(:ci_variable, project: project, key: 'SECRET', value: 'my secret') policy = described_class.new(["$SECRET == 'my secret'"]) diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index c5e7ab959b2..d035df7e0c2 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -15,5 +15,9 @@ describe Gitlab::Git::LfsChanges do it 'limits new_objects using object_limit' do expect(subject.new_pointers(object_limit: 1)).to eq([]) end + + it 'times out if given a small dynamic timeout' do + expect { subject.new_pointers(dynamic_timeout: 0.001) }.to raise_error(GRPC::DeadlineExceeded) + end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index e7da5565c26..a417ef77c9e 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -934,6 +934,16 @@ describe Gitlab::GitAccess do # There is still an N+1 query with protected branches expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(1) end + + it 'raises TimeoutError when #check_single_change_access raises a timeout error' do + message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow" + + expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check| + expect(check).to receive(:exec).and_raise(Gitlab::Checks::TimedLogger::TimeoutError) + end + + expect { access.check('git-receive-pack', changes) }.to raise_error(described_class::TimeoutError, message) + end end end diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index 0385dd762c2..1e583f4cee2 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -11,11 +11,8 @@ describe Gitlab::Identifier do describe '#identify' do context 'without an identifier' do - it 'identifies the user using a commit' do - expect(identifier).to receive(:identify_using_commit) - .with(project, '123') - - identifier.identify('', project, '123') + it 'returns nil' do + expect(identifier.identify('')).to be nil end end @@ -24,7 +21,7 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_user) .with("user-#{user.id}") - identifier.identify("user-#{user.id}", project, '123') + identifier.identify("user-#{user.id}") end end @@ -33,49 +30,11 @@ describe Gitlab::Identifier do expect(identifier).to receive(:identify_using_ssh_key) .with("key-#{key.id}") - identifier.identify("key-#{key.id}", project, '123') + identifier.identify("key-#{key.id}") end end end - describe '#identify_using_commit' do - it "returns the User for an existing commit author's Email address" do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - - it 'returns nil when no user could be found' do - allow(project).to receive(:commit).with('123').and_return(nil) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'returns nil when the commit does not have an author Email' do - commit = double(:commit, author_email: nil) - - expect(project).to receive(:commit).with('123').and_return(commit) - - expect(identifier.identify_using_commit(project, '123')).to be_nil - end - - it 'caches the found users per Email' do - commit = double(:commit, author: user, author_email: user.email) - - expect(project).to receive(:commit).with('123').twice.and_return(commit) - - 2.times do - expect(identifier.identify_using_commit(project, '123')).to eq(user) - end - end - - it 'returns nil if the project & ref are not present' do - expect(identifier.identify_using_commit(nil, nil)).to be_nil - end - end - describe '#identify_using_user' do it 'returns the User for an existing ID in the identifier' do found = identifier.identify_using_user("user-#{user.id}") diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 624e2add860..8df0facdab3 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true end + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + end + + it 'returns true for loopback IP' do + expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (0177.1)' do expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true end @@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do end end + it 'allows localhost endpoints' do + expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true) + end + + it 'allows loopback endpoints' do + expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true) + end + it 'allows IPv4 link-local endpoints' do expect(described_class).not_to be_blocked_url('http://169.254.169.254') expect(described_class).not_to be_blocked_url('http://169.254.168.100') @@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)]) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)]) end def unstub_domain_resolv diff --git a/spec/migrations/enqueue_redact_links_spec.rb b/spec/migrations/enqueue_redact_links_spec.rb new file mode 100644 index 00000000000..a5da76977b7 --- /dev/null +++ b/spec/migrations/enqueue_redact_links_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181014121030_enqueue_redact_links.rb') + +describe EnqueueRedactLinks, :migration, :sidekiq do + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + group = namespaces.create!(name: 'gitlab', path: 'gitlab') + project = projects.create!(namespace_id: group.id) + + merge_requests.create!(id: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master', description: text) + issues.create!(id: 1, description: text) + notes.create!(id: 1, note: text) + notes.create!(id: 2, note: text) + snippets.create!(id: 1, description: text, author_id: user.id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Note", "note", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, "Note", "note", 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Issue", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "MergeRequest", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Snippet", "description", 1, 1) + expect(BackgroundMigrationWorker.jobs.size).to eq 5 + end + end + end +end diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb new file mode 100644 index 00000000000..6d155f78342 --- /dev/null +++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb') + +describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 4) + + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02') + personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest') + personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04') + personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05') + personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + migrate! + + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5)) + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6)) + expect(BackgroundMigrationWorker.jobs.size).to eq 2 + end + end + + it 'schedules background migrations' do + perform_enqueued_jobs do + plain_text_token = 'token IS NOT NULL' + + expect(personal_access_tokens.where(plain_text_token).count).to eq 5 + + migrate! + + expect(personal_access_tokens.where(plain_text_token).count).to eq 0 + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a046541031e..65e06f27f35 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2027,17 +2027,17 @@ describe Ci::Build do it { is_expected.to include(tag_variable) } end - context 'when secret variable is defined' do - let(:secret_variable) do + context 'when CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_variable, - secret_variable.slice(:key, :value).merge(project: project)) + ci_variable.slice(:key, :value).merge(project: project)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when protected variable is defined' do @@ -2072,17 +2072,17 @@ describe Ci::Build do end end - context 'when group secret variable is defined' do - let(:secret_variable) do + context 'when group CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_group_variable, - secret_variable.slice(:key, :value).merge(group: group)) + ci_variable.slice(:key, :value).merge(group: group)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when group protected variable is defined' do @@ -2357,7 +2357,7 @@ describe Ci::Build do .to receive(:predefined_variables) { [project_pre_var] } allow_any_instance_of(Project) - .to receive(:secret_variables_for) + .to receive(:ci_variables_for) .with(ref: 'master', environment: nil) do [create(:ci_variable, key: 'secret', value: 'value')] end @@ -2508,7 +2508,7 @@ describe Ci::Build do end describe '#scoped_variables_hash' do - context 'when overriding secret variables' do + context 'when overriding CI variables' do before do project.variables.create!(key: 'MY_VAR', value: 'my value 1') pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f5c4b0b66ae..c245e8df815 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' describe Clusters::Cluster do it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:cluster_projects) } it { is_expected.to have_many(:projects) } + it { is_expected.to have_many(:cluster_groups) } + it { is_expected.to have_many(:groups) } it { is_expected.to have_one(:provider_gcp) } it { is_expected.to have_one(:platform_kubernetes) } it { is_expected.to have_one(:application_helm) } @@ -178,6 +181,53 @@ describe Clusters::Cluster do it { expect(cluster.update(enabled: false)).to be_truthy } end end + + describe 'cluster_type validations' do + let(:instance_cluster) { create(:cluster, :instance) } + let(:group_cluster) { create(:cluster, :group) } + let(:project_cluster) { create(:cluster, :project) } + + it 'validates presence' do + cluster = build(:cluster, :project, cluster_type: nil) + + expect(cluster).not_to be_valid + expect(cluster.errors.full_messages).to include("Cluster type can't be blank") + end + + context 'project_type cluster' do + it 'does not allow setting group' do + project_cluster.groups << build(:group) + + expect(project_cluster).not_to be_valid + expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + end + + context 'group_type cluster' do + it 'does not allow setting project' do + group_cluster.projects << build(:project) + + expect(group_cluster).not_to be_valid + expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + + context 'instance_type cluster' do + it 'does not allow setting group' do + instance_cluster.groups << build(:group) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + + it 'does not allow setting project' do + instance_cluster.projects << build(:project) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + end end describe '#provider' do @@ -229,6 +279,23 @@ describe Clusters::Cluster do end end + describe '#group' do + subject { cluster.group } + + context 'when cluster belongs to a group' do + let(:cluster) { create(:cluster, :group) } + let(:group) { cluster.groups.first } + + it { is_expected.to eq(group) } + end + + context 'when cluster does not belong to any group' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end + describe '#applications' do set(:cluster) { create(:cluster) } diff --git a/spec/models/clusters/group_spec.rb b/spec/models/clusters/group_spec.rb new file mode 100644 index 00000000000..ba145342cb8 --- /dev/null +++ b/spec/models/clusters/group_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Group do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:group) } +end diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb new file mode 100644 index 00000000000..7d320edd492 --- /dev/null +++ b/spec/models/concerns/redactable_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Redactable do + shared_examples 'model with redactable field' do + it 'redacts unsubscribe token' do + model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + + model.save! + + expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text' + end + + it 'ignores not hexadecimal tokens' do + text = 'some text /sent_notifications/token/unsubscribe more text' + model[field] = text + + model.save! + + expect(model[field]).to eq text + end + + it 'ignores not matching texts' do + text = 'some text /sent_notifications/.*/unsubscribe more text' + model[field] = text + + model.save! + + expect(model[field]).to eq text + end + + it 'redacts the field when saving the model before creating markdown cache' do + model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + + model.save! + + expected = 'some text /sent_notifications/REDACTED/unsubscribe more text' + expect(model[field]).to eq expected + expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>" + end + end + + context 'when model is an issue' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:issue) } + let(:field) { :description } + end + end + + context 'when model is a merge request' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:merge_request) } + let(:field) { :description } + end + end + + context 'when model is a note' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:note) } + let(:field) { :note } + end + end + + context 'when model is a snippet' do + it_behaves_like 'model with redactable field' do + let(:model) { create(:snippet) } + let(:field) { :description } + end + end +end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 9b804429138..782687516ae 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -2,8 +2,6 @@ require 'spec_helper' shared_examples 'TokenAuthenticatable' do describe 'dynamically defined methods' do - it { expect(described_class).to be_private_method_defined(:generate_token) } - it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } it { is_expected.to respond_to("set_#{token_field}") } @@ -66,13 +64,275 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end describe 'multiple token fields' do - before do + before(:all) do described_class.send(:add_authentication_token_field, :yet_another_token) end - describe '.token_fields' do - subject { described_class.authentication_token_fields } - it { is_expected.to include(:runners_registration_token, :yet_another_token) } + it { is_expected.to respond_to(:ensure_runners_registration_token) } + it { is_expected.to respond_to(:ensure_yet_another_token) } + end + + describe 'setting same token field multiple times' do + subject { described_class.send(:add_authentication_token_field, :runners_registration_token) } + + it 'raises error' do + expect {subject}.to raise_error(ArgumentError) + end + end +end + +describe PersonalAccessToken, 'TokenAuthenticatable' do + let(:personal_access_token_name) { 'test-pat-01' } + let(:token_value) { 'token' } + let(:user) { create(:user) } + let(:personal_access_token) do + described_class.new(name: personal_access_token_name, + user_id: user.id, + scopes: [:api], + token: token, + token_digest: token_digest) + end + + before do + allow(Devise).to receive(:friendly_token).and_return(token_value) + end + + describe '.find_by_token' do + subject { PersonalAccessToken.find_by_token(token_value) } + + before do + personal_access_token.save + end + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'finds the token' do + expect(subject).not_to be_nil + expect(subject.name).to eql(personal_access_token_name) + end + end + + context 'token_digest does not exist' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'finds the token' do + expect(subject).not_to be_nil + expect(subject.name).to eql(personal_access_token_name) + end + end + end + + describe '#set_token' do + let(:new_token_value) { 'new-token' } + subject { personal_access_token.set_token(new_token_value) } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'overwrites token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'creates new token_digest and clears token' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(new_token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value)) + end + end + end + + describe '#ensure_token' do + subject { personal_access_token.ensure_token } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to be_nil + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to eql(token_value) + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to be_nil + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + end + + describe '#ensure_token!' do + subject { personal_access_token.ensure_token! } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to be_nil + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { token_value } + let(:token_digest) { nil } + + it 'does not change token fields' do + subject + + expect(personal_access_token.read_attribute('token')).to eql(token_value) + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to be_nil + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + end + + describe '#reset_token!' do + subject { personal_access_token.reset_token! } + + context 'token_digest already exists' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist but token does' do + let(:token) { 'old-token' } + let(:token_digest) { nil } + + it 'creates new token_digest and clears token' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest does not exist, nor token' do + let(:token) { nil } + let(:token_digest) { nil } + + it 'creates new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token_digest exists and newly generated token would be the same' do + let(:token) { nil } + let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') } + + before do + personal_access_token.save + allow(Devise).to receive(:friendly_token).and_return( + 'old-token', token_value, 'boom!') + end + + it 'regenerates a new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end + end + + context 'token exists and newly generated token would be the same' do + let(:token) { 'old-token' } + let(:token_digest) { nil } + + before do + personal_access_token.save + allow(Devise).to receive(:friendly_token).and_return( + 'old-token', token_value, 'boom!') + end + + it 'regenerates a new token_digest' do + subject + + expect(personal_access_token.read_attribute('token')).to be_nil + expect(personal_access_token.token).to eql(token_value) + expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value)) + end end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c65e0b81451..1de95d881a7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -334,7 +334,7 @@ describe Environment do describe '#has_terminals?' do subject { environment.has_terminals? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do context 'and a deployment' do @@ -447,7 +447,7 @@ describe Environment do describe '#has_metrics?' do subject { environment.has_metrics? } - context 'when the enviroment is available' do + context 'when the environment is available' do context 'with a deployment service' do let(:project) { create(:prometheus_project) } @@ -456,8 +456,8 @@ describe Environment do it { is_expected.to be_truthy } end - context 'but no deployments' do - it { is_expected.to be_falsy } + context 'and no deployments' do + it { is_expected.to be_truthy } end end @@ -504,39 +504,6 @@ describe Environment do end end - describe '#has_metrics?' do - subject { environment.has_metrics? } - - context 'when the enviroment is available' do - context 'with a deployment service' do - let(:project) { create(:prometheus_project) } - - context 'and a deployment' do - let!(:deployment) { create(:deployment, environment: environment) } - it { is_expected.to be_truthy } - end - - context 'but no deployments' do - it { is_expected.to be_falsy } - end - end - - context 'without a monitoring service' do - it { is_expected.to be_falsy } - end - end - - context 'when the environment is unavailable' do - let(:project) { create(:prometheus_project) } - - before do - environment.stop - end - - it { is_expected.to be_falsy } - end - end - describe '#additional_metrics' do let(:project) { create(:prometheus_project) } subject { environment.additional_metrics } diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index f2eb263c98c..e7805d52d75 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -5,13 +5,15 @@ describe EnvironmentStatus do let(:environment) { deployment.environment} let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } + let(:sha) { deployment.sha } - subject(:environment_status) { described_class.new(environment, merge_request) } + subject(:environment_status) { described_class.new(environment, merge_request, sha) } it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } it { is_expected.to delegate_method(:project).to(:environment) } it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } + it { is_expected.to delegate_method(:status).to(:deployment) } describe '#project' do subject { environment_status.project } @@ -58,4 +60,32 @@ describe EnvironmentStatus do ) end end + + describe '.for_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + it 'is based on merge_request.head_pipeline' do + expect(merge_request).to receive(:head_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:merge_pipeline) + + described_class.for_merge_request(merge_request, admin) + end + end + + describe '.after_merge_request' do + let(:admin) { create(:admin) } + let(:pipeline) { create(:ci_pipeline, sha: sha) } + + before do + merge_request.mark_as_merged! + end + + it 'is based on merge_request.merge_pipeline' do + expect(merge_request).to receive(:merge_pipeline).and_return(pipeline) + expect(merge_request).not_to receive(:head_pipeline) + + described_class.after_merge_request(merge_request, admin) + end + end end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index ab58f5c5021..b6355455c1d 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -92,41 +92,6 @@ describe GlobalMilestone do end end - describe '.states_count' do - context 'when the projects have milestones' do - before do - create(:closed_milestone, title: 'Active Group Milestone', project: project3) - create(:active_milestone, title: 'Active Group Milestone', project: project1) - create(:active_milestone, title: 'Active Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project1) - create(:closed_milestone, title: 'Closed Group Milestone', project: project2) - create(:closed_milestone, title: 'Closed Group Milestone', project: project3) - end - - it 'returns the quantity of global milestones in each possible state' do - expected_count = { opened: 1, closed: 2, all: 2 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - - context 'when the projects do not have milestones' do - before do - project1 - end - - it 'returns 0 as the quantity of global milestones in each state' do - expected_count = { opened: 0, closed: 0, all: 0 } - - count = described_class.states_count(Project.all) - - expect(count).to eq(expected_count) - end - end - end - describe '#initialize' do let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1bf8f89e126..ada00f03928 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -19,6 +19,8 @@ describe Group do it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:badges).class_name('GroupBadge') } + it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') } + it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') } describe '#members & #requesters' do let(:requester) { create(:user) } @@ -651,10 +653,10 @@ describe Group do end end - describe '#secret_variables_for' do + describe '#ci_variables_for' do let(:project) { create(:project, group: group) } - let!(:secret_variable) do + let!(:ci_variable) do create(:ci_group_variable, value: 'secret', group: group) end @@ -662,11 +664,11 @@ describe Group do create(:ci_group_variable, :protected, value: 'protected', group: group) end - subject { group.secret_variables_for('ref', project) } + subject { group.ci_variables_for('ref', project) } shared_examples 'ref is protected' do it 'contains all the variables' do - is_expected.to contain_exactly(secret_variable, protected_variable) + is_expected.to contain_exactly(ci_variable, protected_variable) end end @@ -676,8 +678,8 @@ describe Group do default_branch_protection: Gitlab::Access::PROTECTION_NONE) end - it 'contains only the secret variables' do - is_expected.to contain_exactly(secret_variable) + it 'contains only the CI variables' do + is_expected.to contain_exactly(ci_variable) end end @@ -710,9 +712,9 @@ describe Group do end it 'returns all variables belong to the group and parent groups' do - expected_array1 = [protected_variable, secret_variable] + expected_array1 = [protected_variable, ci_variable] expected_array2 = [variable_child, variable_child_2, variable_child_3] - got_array = group_child_3.secret_variables_for('ref', project).to_a + got_array = group_child_3.ci_variables_for('ref', project).to_a expect(got_array.shift(2)).to contain_exactly(*expected_array1) expect(got_array).to eq(expected_array2) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 666d7e69f89..c8943f2d86f 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1058,6 +1058,26 @@ describe MergeRequest do end end + describe '#merge_pipeline' do + it 'returns nil when not merged' do + expect(subject.merge_pipeline).to be_nil + end + + context 'when the MR is merged' do + let(:sha) { subject.target_project.commit.id } + let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) } + + before do + subject.mark_as_merged! + subject.update_attribute(:merge_commit_sha, pipeline.sha) + end + + it 'returns the post-merge pipeline' do + expect(subject.merge_pipeline).to eq(pipeline) + end + end + end + describe '#has_ci?' do let(:merge_request) { build_stubbed(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 27d4e622710..d11eb46159e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -348,4 +348,41 @@ describe Milestone do end end end + + describe '.states_count' do + context 'when the projects have milestones' do + before do + project_1 = create(:project) + project_2 = create(:project) + group_1 = create(:group) + group_2 = create(:group) + + create(:active_milestone, title: 'Active Group Milestone', project: project_1) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_1) + create(:active_milestone, title: 'Active Group Milestone', project: project_2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project_2) + create(:closed_milestone, title: 'Active Group Milestone', group: group_1) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_1) + create(:closed_milestone, title: 'Active Group Milestone', group: group_2) + create(:closed_milestone, title: 'Closed Group Milestone', group: group_2) + end + + it 'returns the quantity of milestones in each possible state' do + expected_count = { opened: 5, closed: 6, all: 11 } + + count = described_class.states_count(Project.all, Group.all) + expect(count).to eq(expected_count) + end + end + + context 'when the projects do not have milestones' do + it 'returns 0 as the quantity of global milestones in each state' do + expected_count = { opened: 0, closed: 0, all: 0 } + + count = described_class.states_count([project]) + + expect(count).to eq(expected_count) + end + end + end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 2bb1c49b740..c82ab9c9e62 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -49,18 +49,36 @@ describe PersonalAccessToken do describe 'Redis storage' do let(:user_id) { 123 } - let(:token) { 'abc000foo' } + let(:token) { 'KS3wegQYXBLYhQsciwsj' } - before do - subject.redis_store!(user_id, token) + context 'reading encrypted data' do + before do + subject.redis_store!(user_id, token) + end + + it 'returns stored data' do + expect(subject.redis_getdel(user_id)).to eq(token) + end end - it 'returns stored data' do - expect(subject.redis_getdel(user_id)).to eq(token) + context 'reading unencrypted data' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set(described_class.redis_shared_state_key(user_id), + token, + ex: PersonalAccessToken::REDIS_EXPIRY_TIME) + end + end + + it 'returns stored data unmodified' do + expect(subject.redis_getdel(user_id)).to eq(token) + end end context 'after deletion' do before do + subject.redis_store!(user_id, token) + expect(subject.redis_getdel(user_id)).to eq(token) end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 0cd712e2f40..b0fd2ceead0 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -387,4 +387,22 @@ describe HipchatService do end end end + + context 'with UrlBlocker' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:hipchat) { described_class.new(project: project) } + let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + + describe '#execute' do + before do + hipchat.server = 'http://localhost:9123' + end + + it 'raises UrlBlocker for localhost' do + expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original + expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 62a38c66d99..e66838edd1a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2432,10 +2432,10 @@ describe Project do end end - describe '#secret_variables_for' do + describe '#ci_variables_for' do let(:project) { create(:project) } - let!(:secret_variable) do + let!(:ci_variable) do create(:ci_variable, value: 'secret', project: project) end @@ -2443,7 +2443,7 @@ describe Project do create(:ci_variable, :protected, value: 'protected', project: project) end - subject { project.reload.secret_variables_for(ref: 'ref') } + subject { project.reload.ci_variables_for(ref: 'ref') } before do stub_application_setting( @@ -2452,13 +2452,13 @@ describe Project do shared_examples 'ref is protected' do it 'contains all the variables' do - is_expected.to contain_exactly(secret_variable, protected_variable) + is_expected.to contain_exactly(ci_variable, protected_variable) end end context 'when the ref is not protected' do - it 'contains only the secret variables' do - is_expected.to contain_exactly(secret_variable) + it 'contains only the CI variables' do + is_expected.to contain_exactly(ci_variable) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b3474e74aa4..4e7c8523e65 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -730,6 +730,14 @@ describe User do expect(user.incoming_email_token).not_to be_blank end + + it 'uses SecureRandom to generate the incoming email token' do + expect(SecureRandom).to receive(:hex).and_return('3b8ca303') + + user = create(:user) + + expect(user.incoming_email_token).to eql('gitlab') + end end describe '#ensure_user_rights_and_limits' do diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index a1b52d8692d..bafcddebbb7 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -403,6 +403,15 @@ describe MergeRequestPresenter do is_expected .to eq("<a href=\"/#{resource.source_project.full_path}/tree/#{resource.source_branch}\">#{resource.source_branch}</a>") end + + it 'escapes html, when source_branch does not exist' do + xss_attempt = "<img src='x' onerror=alert('bad stuff') />" + + allow(resource).to receive(:source_branch) { xss_attempt } + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to eq(ERB::Util.html_escape(xss_attempt)) + end end describe '#rebase_path' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index e0b5b34f9c4..5ea869796b0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -494,6 +494,24 @@ describe API::Internal do end end + context 'request times out' do + context 'git push' do + it 'responds with a gateway timeout' do + personal_project = create(:project, namespace: user.namespace) + + expect_next_instance_of(Gitlab::GitAccess) do |access| + expect(access).to receive(:check).and_raise(Gitlab::GitAccess::TimeoutError, "Foo") + end + push(key, personal_project) + + expect(response).to have_gitlab_http_status(503) + expect(json_response['status']).to be_falsey + expect(json_response['message']).to eq("Foo") + expect(user.reload.last_activity_on).to be_nil + end + end + end + context "archived project" do before do project.add_developer(user) @@ -665,7 +683,7 @@ describe API::Internal do expect(json_response).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end @@ -686,7 +704,7 @@ describe API::Internal do expect(json_response).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end @@ -819,7 +837,7 @@ describe API::Internal do expect(json_response['merge_request_urls']).to match [{ "branch_name" => "new_branch", - "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch", "new_merge_request" => true }] end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index c0d5a3ad74b..909703a8d47 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -806,6 +806,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_unknown_failure } end + + context 'when failure_reason is job_execution_timeout' do + before do + update_job(state: 'failed', failure_reason: 'job_execution_timeout') + job.reload + end + + it { expect(job).to be_job_execution_timeout } + end end context 'when trace is given' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index c40d01e1a14..08bada44178 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -158,6 +158,16 @@ describe API::Wikis do expect(json_response.size).to eq(1) expect(json_response['error']).to eq('file is missing') end + + it 'responds with validation error on invalid temp file' do + payload[:file] = { tempfile: '/etc/hosts' } + + post(api(url, user), payload) + + expect(response).to have_gitlab_http_status(400) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is invalid') + end end describe 'GET /projects/:id/wikis' do diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 6894c65d639..1b4d8b70aa6 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } - let(:environment_status) { EnvironmentStatus.new(environment, merge_request) } + let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) } let(:entity) { described_class.new(environment_status, request: request) } subject { entity.as_json } @@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do it { is_expected.to include(:deployed_at) } it { is_expected.to include(:deployed_at_formatted) } it { is_expected.to include(:changes) } + it { is_expected.to include(:status) } it { is_expected.not_to include(:stop_url) } it { is_expected.not_to include(:metrics_url) } diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 5bf8aa7f23f..561421d5ac8 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do end end + describe 'merge_pipeline' do + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + + context 'when is merged' do + let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) } + + before do + project.add_maintainer(user) + end + + it 'returns merge_pipeline' do + pipeline.reload + pipeline_payload = PipelineDetailsEntity + .represent(pipeline, request: request) + .as_json + + expect(subject[:merge_pipeline]).to eq(pipeline_payload) + end + + context 'when user cannot read pipelines on target project' do + before do + project.add_guest(user) + end + + it 'returns nil' do + expect(subject[:merge_pipeline]).to be_nil + end + end + end + end + describe 'metrics' do context 'when metrics record exists with merged data' do before do diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 274624aa8bb..3e33a165e55 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } @@ -117,7 +117,7 @@ describe MergeRequests::GetUrlsService do let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } - let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch" } it 'returns 2 urls for both creating new and showing merge request' do result = service.execute(changes) diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2536c6e2514..61c6ba7d550 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -306,6 +306,66 @@ describe MergeRequests::RefreshService do end end + context 'forked projects with the same source branch name as target branch' do + let!(:first_commit) do + @fork_project.repository.create_file(@user, 'test1.txt', 'Test data', + message: 'Test commit', + branch_name: 'master') + end + let!(:second_commit) do + @fork_project.repository.create_file(@user, 'test2.txt', 'More test data', + message: 'Second test commit', + branch_name: 'master') + end + let!(:forked_master_mr) do + create(:merge_request, + source_project: @fork_project, + source_branch: 'master', + target_branch: 'master', + target_project: @project) + end + let(:force_push_commit) { @project.commit('feature').id } + + it 'should reload a new diff for a push to the forked project' do + expect do + service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a force push to the source branch' do + expect do + service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a force push to the target branch' do + expect do + service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should reload a new diff for a push to the target project that contains a commit in the MR' do + expect do + service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master') + reload_mrs + end.to change { forked_master_mr.merge_request_diffs.count }.by(1) + end + + it 'should not increase the diff count for a new push to target branch' do + new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file', + message: 'This is a test', + branch_name: 'master') + + expect do + service.new(@project, @user).execute(@newrev, new_commit, 'refs/heads/master') + reload_mrs + end.not_to change { forked_master_mr.merge_request_diffs.count } + end + end + context 'push to origin repo target branch after fork project was removed' do before do @fork_project.destroy diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index f7f851eb1eb..bce1fb01355 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -5,7 +5,7 @@ shared_examples 'variable list' do end end - it 'adds new secret variable' do + it 'adds new CI variable' do page.within('.js-ci-variable-list-section .js-row:last-child') do find('.js-ci-variable-input-key').set('key') find('.js-ci-variable-input-value').set('key value') diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 7038a366144..9373de5aeab 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -17,10 +17,10 @@ RSpec.shared_examples 'a creatable merge request' do sign_in(user) visit project_new_merge_request_path( target_project, + merge_request_source_branch: 'fix', merge_request: { source_project_id: source_project.id, target_project_id: target_project.id, - source_branch: 'fix', target_branch: 'master' }) end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index cd6661f09a1..9176eb12b12 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -6,7 +6,7 @@ describe PostReceive do let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } let(:gl_repository) { "project-#{project.id}" } let(:key) { create(:key, user: project.owner) } - let(:key_id) { key.shell_id } + let!(:key_id) { key.shell_id } let(:project) do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') @@ -31,85 +31,108 @@ describe PostReceive do end describe "#process_project_changes" do - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + context 'empty changes' do + it "does not call any PushService but runs after project hooks" do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } + + described_class.new.perform(gl_repository, key_id, "") + end end - context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + context 'unidentified user' do + let!(:key_id) { "" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it 'returns false' do + expect(GitPushService).not_to receive(:new) + expect(GitTagPushService).not_to receive(:new) + + expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false end end - context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + context 'with changes' do + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + end + + context "branches" do + let(:changes) { "123456 789012 refs/heads/tést" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitPushService" do + expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "merge-requests" do - let(:changes) { "123456 789012 refs/merge-requests/123" } + context "tags" do + let(:changes) { "123456 789012 refs/tags/tag" } - it "does not call any of the services" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) - described_class.new.perform(gl_repository, key_id, base64_changes) + it "calls GitTagPushService" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end - end - context "gitlab-ci.yml" do - let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } + context "merge-requests" do + let(:changes) { "123456 789012 refs/merge-requests/123" } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + it "does not call any of the services" do + expect_any_instance_of(GitPushService).not_to receive(:execute) + expect_any_instance_of(GitTagPushService).not_to receive(:execute) + described_class.new.perform(gl_repository, key_id, base64_changes) + end + end - context "creates a Ci::Pipeline for every change" do - before do - stub_ci_pipeline_to_return_yaml_file + context "gitlab-ci.yml" do + let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" } - allow_any_instance_of(Project) - .to receive(:commit) - .and_return(project.commit) + subject { described_class.new.perform(gl_repository, key_id, base64_changes) } - allow_any_instance_of(Repository) - .to receive(:branch_exists?) - .and_return(true) - end + context "creates a Ci::Pipeline for every change" do + before do + stub_ci_pipeline_to_return_yaml_file - it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } - end + allow_any_instance_of(Project) + .to receive(:commit) + .and_return(project.commit) - context "does not create a Ci::Pipeline" do - before do - stub_ci_pipeline_yaml_file(nil) + allow_any_instance_of(Repository) + .to receive(:branch_exists?) + .and_return(true) + end + + it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } end - it { expect { subject }.not_to change { Ci::Pipeline.count } } + context "does not create a Ci::Pipeline" do + before do + stub_ci_pipeline_yaml_file(nil) + end + + it { expect { subject }.not_to change { Ci::Pipeline.count } } + end end - end - context 'after project changes hooks' do - let(:changes) { '123456 789012 refs/heads/tést' } - let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) - # silence hooks so we can isolate - allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) - end + before do + allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) + # silence hooks so we can isolate + allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) + allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + described_class.new.perform(gl_repository, key_id, base64_changes) + end end end end diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml index 049ffcc3407..24136a7aca5 100644 --- a/vendor/jupyter/values.yaml +++ b/vendor/jupyter/values.yaml @@ -16,7 +16,7 @@ singleuser: lifecycleHooks: postStart: exec: - command: ["git", "clone", "https://gitlab.com/gitlab-org/nurtch-demo.git", "DevOps-Runbook-Demo"] + command: ["sh", "-c", "git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true"] ingress: enabled: true diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml index c432be72163..02ec3e2d9fe 100644 --- a/vendor/prometheus/values.yaml +++ b/vendor/prometheus/values.yaml @@ -1,5 +1,7 @@ alertmanager: enabled: false + image: + tag: v0.15.2 kubeStateMetrics: enabled: true @@ -16,7 +18,7 @@ rbac: server: fullnameOverride: "prometheus-prometheus-server" image: - tag: v2.1.0 + tag: v2.4.3 serverFiles: alerts: {} diff --git a/yarn.lock b/yarn.lock index 5da401c1d43..79f1b757252 100644 --- a/yarn.lock +++ b/yarn.lock @@ -626,10 +626,10 @@ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a" integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA== -"@gitlab-org/gitlab-ui@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee" - integrity sha512-Owm8bkP4vEihiLD3pmMw1r+UWr3WYGaGUtj0JcwaAg3d05ZneozFEZjazIOWeYTcFsk+ZvNmSk1UA+ARIauhgQ== +"@gitlab-org/gitlab-ui@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.9.0.tgz#c47851587316f60926e8304747d1fcdd1222c779" + integrity sha512-OQ/mhWnbeG4pmjnCGwLsyvmHDYdLh2IRnt4Jx6G9jf96oyjEHzY1rveImfqcQ2bvx9azfuI6CU9dmDSY3aWvvQ== dependencies: "@gitlab-org/gitlab-svgs" "^1.23.0" bootstrap-vue "^2.0.0-rc.11" @@ -5502,12 +5502,7 @@ mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: dependencies: minimist "0.0.8" -moment@2.x: - version "2.19.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" - integrity sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA== - -moment@^2.21.0: +moment@2.x, moment@^2.21.0: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= |