diff options
696 files changed, 9984 insertions, 4111 deletions
diff --git a/.gitignore b/.gitignore index 82b3d08f7a8..aecaae95b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ eslint-report.html /config/redis.queues.yml /config/redis.shared_state.yml /config/unicorn.rb +/config/puma.rb /config/secrets.yml /config/sidekiq.yml /config/registry.key diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b03b5c42376..0e7a67f9cc1 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 @@ -1019,6 +1019,5 @@ schedule:review_apps_cleanup: - schedules@gitlab-org/gitlab-ee kubernetes: active except: - - master - tags - /(^docs[\/-].*|.*-docs$)/ diff --git a/.rubocop.yml b/.rubocop.yml index 0f4018326a1..a95ded8af1c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -76,10 +76,14 @@ Naming/FileName: - 'qa/qa/specs/**/*' - 'qa/bin/*' - 'config/**/*' + - 'ee/config/**/*' - 'lib/generators/**/*' - '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 @@ -153,6 +153,11 @@ group :unicorn do gem 'unicorn-worker-killer', '~> 0.4.4' end +group :puma do + gem 'puma', '~> 3.12', require: false + gem 'puma_worker_killer', require: false +end + # State machine gem 'state_machines-activerecord', '~> 0.5.1' @@ -239,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 bf16bef4f32..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.0) + parser (2.5.3.0) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -598,6 +597,10 @@ GEM pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.3) + puma (3.12.0) + puma_worker_killer (0.1.0) + get_process_mem (~> 0.2) + puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) rack (1.6.10) rack-accept (0.4.5) @@ -1042,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) @@ -1076,6 +1078,8 @@ DEPENDENCIES prometheus-client-mmap (~> 0.9.4) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) + puma (~> 3.12) + puma_worker_killer rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 81547303ed2..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) @@ -551,7 +550,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.0) + parser (2.5.1.2) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -602,6 +601,10 @@ GEM pry-rails (0.3.6) pry (>= 0.10.4) public_suffix (3.0.3) + puma (3.12.0) + puma_worker_killer (0.1.0) + get_process_mem (~> 0.2) + puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) rack (2.0.5) rack-accept (0.4.5) @@ -1051,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) @@ -1085,6 +1087,8 @@ DEPENDENCIES prometheus-client-mmap (~> 0.9.4) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) + puma (~> 3.12) + puma_worker_killer rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 75477ebb3b3..623cda5679a 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -53,6 +53,9 @@ export default Vue.extend({ const { issuesSize } = this.list; return `${n__('%d issue', '%d issues', issuesSize)}`; }, + isNewIssueShown() { + return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); + } }, watch: { filter: { 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/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 4e8fe16160a..427a0868b0c 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -46,7 +46,7 @@ export default { selectable: true, data: (term, callback) => { this.loading = true; - return Api.groupProjects(this.groupId, term, {}, projects => { + return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => { this.loading = false; callback(projects); }); 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..fe551480167 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,17 +1,11 @@ import Vue from 'vue'; import { - Pagination, - ProgressBar, - Modal, - LoadingIcon, - ModalDirective, - TooltipDirective, + GlProgressBar, + GlLoadingIcon, + 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-progress-bar', GlProgressBar); +Vue.component('gl-loading-icon', GlLoadingIcon); -Vue.directive('gl-modal', ModalDirective); -Vue.directive('gl-tooltip', TooltipDirective); +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/group.js b/app/assets/javascripts/group.js index 4365305c168..903c838e266 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { slugifyWithHyphens } from './lib/utils/text_utility'; export default class Group { constructor() { @@ -7,17 +8,18 @@ export default class Group { this.updateHandler = this.update.bind(this); this.resetHandler = this.reset.bind(this); if (this.groupName.val() === '') { - this.groupPath.on('keyup', this.updateHandler); - this.groupName.on('keydown', this.resetHandler); + this.groupName.on('keyup', this.updateHandler); + this.groupPath.on('keydown', this.resetHandler); } } update() { - this.groupName.val(this.groupPath.val()); + const slug = slugifyWithHyphens(this.groupName.val()); + this.groupPath.val(slug); } reset() { - this.groupPath.off('keyup', this.updateHandler); - this.groupName.off('keydown', this.resetHandler); + this.groupName.off('keyup', this.updateHandler); + this.groupPath.off('keydown', this.resetHandler); } } diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index ad6151e3bf6..0a368f6558c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -43,7 +43,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -63,7 +63,7 @@ export default { onBeforeUnload(e = {}) { const returnValue = __('Are you sure you want to lose unsaved changes?'); - if (!this.someUncommitedChanges) return undefined; + if (!this.someUncommittedChanges) return undefined; Object.assign(e, { returnValue, diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index dc84ee12f1e..364ab9426e0 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, @@ -25,11 +25,11 @@ export default { }, computed: { ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), - ...mapGetters(['currentProject', 'someUncommitedChanges']), + ...mapGetters(['currentProject', 'someUncommittedChanges']), showSuccessMessage() { return ( this.currentActivityView === activityBarViews.edit && - (this.lastCommitMsg && !this.someUncommitedChanges) + (this.lastCommitMsg && !this.someUncommittedChanges) ); }, }, @@ -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/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index d3b24c5b793..5e86876c1c1 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -27,10 +27,10 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']), ...mapGetters('commit', ['discardDraftButtonDisabled']), showStageUnstageArea() { - return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); + return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal); }, activeFileKey() { return this.activeFile ? this.activeFile.key : null; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 709748fb530..8ad85074d6b 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; -export const someUncommitedChanges = state => +export const someUncommittedChanges = state => !!(state.changedFiles.length || state.stagedFiles.length); export const getChangesInFolder = state => path => { diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 906769ee6a2..28a02230d89 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -31,7 +31,7 @@ export default { }, }, computed: { - ...mapState(['job', 'stages', 'jobs', 'selectedStage']), + ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']), coverage() { return `${this.job.coverage}%`; }, @@ -59,10 +59,10 @@ export default { return ''; } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } return t; }, @@ -270,6 +270,7 @@ export default { /> <stages-dropdown + v-if="!isLoadingStages" :stages="stages" :pipeline="job.pipeline" :selected-stage="selectedStage" diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index e5e1d56e287..dc26b246d71 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -22,7 +22,6 @@ export default { required: true, }, }, - computed: { hasRef() { return !_.isEmpty(this.pipeline.ref); diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 4195d787f12..cd440d21c1f 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -71,7 +71,7 @@ export default { * after the first request, * and we do not want to hijack that */ - if (state.selectedStage === 'More' && job.stage) { + if (state.selectedStage === '' && job.stage) { state.selectedStage = job.stage; } }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 0eb269ca38f..04825187c99 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,5 +1,3 @@ -import { __ } from '~/locale'; - export default () => ({ jobEndpoint: null, traceEndpoint: null, @@ -29,7 +27,7 @@ export default () => ({ // sidebar dropdown & list of jobs isLoadingStages: false, isLoadingJobs: false, - selectedStage: __('More'), + selectedStage: '', stages: [], jobs: [], }); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 1bdf98d0c97..46740308f17 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -473,3 +473,15 @@ export const stringifyTime = timeObject => { */ export const abbreviateTime = timeStr => timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0]; + +/** + * Calculates the milliseconds between now and a given date string. + * The result cannot become negative. + * + * @param endDate date string that the time difference is calculated for + * @return {number} number of milliseconds remaining until the given date + */ +export const calculateRemainingMilliseconds = endDate => { + const remainingMilliseconds = new Date(endDate).getTime() - Date.now(); + return Math.max(remainingMilliseconds, 0); +}; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index e26a6b986be..c52cfb806a2 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -2,6 +2,8 @@ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; +const LINK_TAG_PATTERN = '[{text}](url)'; + function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } @@ -76,6 +78,21 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr removedFirstNewLine = false; currentLineEmpty = false; + // check for link pattern and selected text is an URL + // if so fill in the url part instead of the text part of the pattern. + if (tag === LINK_TAG_PATTERN) { + if (URL) { + try { + const ignoredUrl = new URL(selected); + // valid url + tag = '[text]({text})'; + select = 'text'; + } catch (e) { + // ignore - no valid url + } + } + } + // Remove the first newline if (selected.indexOf('\n') === 0) { removedFirstNewLine = true; 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/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index ed5c8b15945..5c6e2e09e46 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -121,6 +121,7 @@ export default { draw() { const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; + const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; this.margin = measurements.large.margin; if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { this.graphHeight = 300; @@ -130,13 +131,13 @@ export default { this.unitOfDisplay = query.unit || ''; this.yAxisLabel = this.graphData.y_label || 'Values'; this.legendTitle = query.label || 'Average'; - this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; + this.graphWidth = svgWidth - this.margin.left - this.margin.right; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.baseGraphHeight = this.graphHeight - 50; this.baseGraphWidth = this.graphWidth; // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = this.$refs.baseSvg.clientWidth / this.baseGraphWidth; + this.realPixelRatio = svgWidth / this.baseGraphWidth; this.renderAxesPaths(); this.formatDeployments(); 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/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index 15e737fff05..d9cf62db3f7 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -31,7 +31,7 @@ export default class AbuseReports { $messageCellElement.text(originalMessage); } else { $messageCellElement.data('messageTruncated', 'true'); - $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`); + $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`); } } } diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index ff4d6ab15f9..4616a075729 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -23,7 +23,7 @@ export default function adminInit() { } }); - $('body').on('click', '.js-toggle-colors-link', (e) => { + $('body').on('click', '.js-toggle-colors-link', e => { e.preventDefault(); $('.js-toggle-colors-container').toggleClass('hide'); }); @@ -33,12 +33,15 @@ export default function adminInit() { $(this).tab('show'); }); - $('.log-bottom').on('click', (e) => { + $('.log-bottom').on('click', e => { e.preventDefault(); const $visibleLog = $('.file-content:visible'); - $visibleLog.animate({ - scrollTop: $visibleLog.find('ol').height(), - }, 'fast'); + $visibleLog.animate( + { + scrollTop: $visibleLog.find('ol').height(), + }, + 'fast', + ); }); $('.change-owner-link').on('click', function changeOwnerLinkClick(e) { @@ -47,7 +50,7 @@ export default function adminInit() { modal.show(); }); - $('.change-owner-cancel-link').on('click', (e) => { + $('.change-owner-cancel-link').on('click', e => { e.preventDefault(); modal.hide(); $('.change-owner-link').show(); diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js index 7281f907ec7..455c637a6b3 100644 --- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -1,10 +1,14 @@ import { __ } from '~/locale'; export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE = __('Regex pattern'); -export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __('To define internal users, first enable new users set to external'); +export const PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE = __( + 'To define internal users, first enable new users set to external', +); function setUserInternalRegexPlaceholder(checkbox) { - const userInternalRegex = document.getElementById('application_setting_user_default_internal_regex'); + const userInternalRegex = document.getElementById( + 'application_setting_user_default_internal_regex', + ); if (checkbox && userInternalRegex) { if (checkbox.checked) { userInternalRegex.readOnly = false; diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index e7ceccb6f47..d5ded3f9a79 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -17,20 +17,24 @@ export default () => { const previewPath = $('textarea#broadcast_message_message').data('previewPath'); - $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() { - const message = $(this).val(); - if (message === '') { - $('.js-broadcast-message-preview').text('Your message here'); - } else { - axios.post(previewPath, { - broadcast_message: { - message, - }, - }) - .then(({ data }) => { - $('.js-broadcast-message-preview').html(data.message); - }) - .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); - } - }, 250)); + $('textarea#broadcast_message_message').on( + 'input', + _.debounce(function onMessageInput() { + const message = $(this).val(); + if (message === '') { + $('.js-broadcast-message-preview').text('Your message here'); + } else { + axios + .post(previewPath, { + broadcast_message: { + message, + }, + }) + .then(({ data }) => { + $('.js-broadcast-message-preview').html(data.message); + }) + .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); + } + }, 250), + ); }; diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index bc84666779e..e2fec3c7172 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -1,39 +1,42 @@ <script> - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { redirectTo } from '~/lib/utils/url_utility'; - import { s__ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { s__ } from '~/locale'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, + }, + computed: { + text() { + return s__( + 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.', + ); }, - computed: { - text() { - return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.'); - }, + }, + methods: { + onSubmit() { + return axios + .post(this.url) + .then(response => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch(error => { + createFlash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); }, - methods: { - onSubmit() { - return axios.post(this.url) - .then((response) => { - // follow the rediect to refresh the page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - createFlash(s__('AdminArea|Stopping jobs failed')); - throw error; - }); - }, - }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 31c96eb87af..d6b1e747aec 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -4,6 +4,7 @@ import NamespaceSelect from '../../../namespace_select'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - document.querySelectorAll('.js-namespace-select') + document + .querySelectorAll('.js-namespace-select') .forEach(dropdown => new NamespaceSelect({ dropdown })); }); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index ff66d3a8ac4..3c383735f4a 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -1,81 +1,84 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteProjectUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteProjectUrl: { - type: String, - required: false, - default: '', - }, - projectName: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + projectName: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredProjectName: '', - }; + csrfToken: { + type: String, + required: false, + default: '', }, - computed: { - title() { - return sprintf(s__('AdminProjects|Delete Project %{projectName}?'), - { - projectName: `'${_.escape(this.projectName)}'`, - }, - false, - ); - }, - text() { - return sprintf(s__(`AdminProjects| + }, + data() { + return { + enteredProjectName: '', + }; + }, + computed: { + title() { + return sprintf( + s__('AdminProjects|Delete Project %{projectName}?'), + { + projectName: `'${_.escape(this.projectName)}'`, + }, + false, + ); + }, + text() { + return sprintf( + s__(`AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`), - { - projectName: `<strong>${_.escape(this.projectName)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{projectName}'), - { - projectName: `<code>${_.escape(this.projectName)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - return s__('AdminProjects|Delete project'); - }, - canSubmit() { - return this.enteredProjectName === this.projectName; - }, + { + projectName: `<strong>${_.escape(this.projectName)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{projectName}'), + { + projectName: `<code>${_.escape(this.projectName)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + return s__('AdminProjects|Delete project'); + }, + canSubmit() { + return this.enteredProjectName === this.projectName; + }, + }, + methods: { + onCancel() { + this.enteredProjectName = ''; }, - methods: { - onCancel() { - this.enteredProjectName = ''; - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredProjectName = ''; - }, + onSubmit() { + this.$refs.form.submit(); + this.enteredProjectName = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index ddbefec87b6..6fa8760545d 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-project-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index 8d5efcdcd96..4b33fcc759a 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -1,114 +1,119 @@ <script> - import _ from 'underscore'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { s__, sprintf } from '~/locale'; +import _ from 'underscore'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { s__, sprintf } from '~/locale'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + deleteUserUrl: { + type: String, + required: false, + default: '', }, - props: { - deleteUserUrl: { - type: String, - required: false, - default: '', - }, - blockUserUrl: { - type: String, - required: false, - default: '', - }, - deleteContributions: { - type: Boolean, - required: false, - default: false, - }, - username: { - type: String, - required: false, - default: '', - }, - csrfToken: { - type: String, - required: false, - default: '', - }, + blockUserUrl: { + type: String, + required: false, + default: '', }, - data() { - return { - enteredUsername: '', - }; + deleteContributions: { + type: Boolean, + required: false, + default: false, }, - computed: { - title() { - const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); - const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); + username: { + type: String, + required: false, + default: '', + }, + csrfToken: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + enteredUsername: '', + }; + }, + computed: { + title() { + const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); + const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); - return sprintf( - this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, { - username: `'${_.escape(this.username)}'`, - }, false); - }, - text() { - const keepContributionsText = s__(`AdminArea| + return sprintf( + this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, + { + username: `'${_.escape(this.username)}'`, + }, + false, + ); + }, + text() { + const keepContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - const deleteContributionsText = s__(`AdminArea| + const deleteContributionsText = s__(`AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, - { - username: `<strong>${_.escape(this.username)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf(s__('AdminUsers|To confirm, type %{username}'), - { - username: `<code>${_.escape(this.username)}</code>`, - }, - false, - ); - }, - primaryButtonLabel() { - const keepContributionsLabel = s__('AdminUsers|Delete user'); - const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); + return sprintf( + this.deleteContributions ? deleteContributionsText : keepContributionsText, + { + username: `<strong>${_.escape(this.username)}</strong>`, + strong_start: '<strong>', + strong_end: '</strong>', + }, + false, + ); + }, + confirmationTextLabel() { + return sprintf( + s__('AdminUsers|To confirm, type %{username}'), + { + username: `<code>${_.escape(this.username)}</code>`, + }, + false, + ); + }, + primaryButtonLabel() { + const keepContributionsLabel = s__('AdminUsers|Delete user'); + const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); - return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; - }, - secondaryButtonLabel() { - return s__('AdminUsers|Block user'); - }, - canSubmit() { - return this.enteredUsername === this.username; - }, + return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; }, - methods: { - onCancel() { - this.enteredUsername = ''; - }, - onSecondaryAction() { - const { form } = this.$refs; + secondaryButtonLabel() { + return s__('AdminUsers|Block user'); + }, + canSubmit() { + return this.enteredUsername === this.username; + }, + }, + methods: { + onCancel() { + this.enteredUsername = ''; + }, + onSecondaryAction() { + const { form } = this.$refs; - form.action = this.blockUserUrl; - this.$refs.method.value = 'put'; + form.action = this.blockUserUrl; + this.$refs.method.value = 'put'; - form.submit(); - }, - onSubmit() { - this.$refs.form.submit(); - this.enteredUsername = ''; - }, + form.submit(); + }, + onSubmit() { + this.$refs.form.submit(); + this.enteredUsername = ''; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 06599c3fd5f..45046688b57 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -32,12 +32,14 @@ document.addEventListener('DOMContentLoaded', () => { }, }); - $(document).on('shown.bs.modal', (event) => { + $(document).on('shown.bs.modal', event => { if (event.relatedTarget.classList.contains('delete-user-button')) { const buttonProps = event.relatedTarget.dataset; deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; deleteModal.blockUserUrl = buttonProps.blockUserUrl; - deleteModal.deleteContributions = event.relatedTarget.hasAttribute('data-delete-contributions'); + deleteModal.deleteContributions = event.relatedTarget.hasAttribute( + 'data-delete-contributions', + ); deleteModal.username = buttonProps.username; } }); diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js index 58bfa8d64e7..3e6a090cb0e 100644 --- a/app/assets/javascripts/pages/admin/users/new/index.js +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -4,7 +4,9 @@ export default class UserInternalRegexHandler { constructor() { this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern'); if (this.regexPattern && this.regexPattern !== '') { - this.regexOptions = $('[data-user-internal-regex-options]').data('user-internal-regex-options'); + this.regexOptions = $('[data-user-internal-regex-options]').data( + 'user-internal-regex-options', + ); this.external = $('#user_external'); this.warningMessage = $('#warning_external_automatically_set'); this.addListenerToEmailField(); @@ -13,7 +15,7 @@ export default class UserInternalRegexHandler { } addListenerToEmailField() { - $('#user_email').on('input', (event) => { + $('#user_email').on('input', event => { this.setExternalCheckbox(event.currentTarget.value); }); } diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 72f3f70b98f..1b56b97f751 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -79,7 +79,8 @@ export default class Todos { .then(({ data }) => { this.updateRowState(target); this.updateBadges(data); - }).catch(() => { + }) + .catch(() => { this.updateRowState(target, true); return flash(__('Error updating todo status.')); }); @@ -118,10 +119,12 @@ export default class Todos { axios[target.dataset.method](target.dataset.href, { ids: this.todo_ids, - }).then(({ data }) => { - this.updateAllState(target, data); - this.updateBadges(data); - }).catch(() => flash(__('Error updating status for all todos.'))); + }) + .then(({ data }) => { + this.updateAllState(target, data); + this.updateBadges(data); + }) + .catch(() => flash(__('Error updating status for all todos.'))); } updateAllState(target, data) { @@ -133,7 +136,7 @@ export default class Todos { target.removeAttribute('disabled'); target.classList.remove('disabled'); - this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : []; + this.todo_ids = target === markAllDoneBtn ? data.updated_ids : []; undoAllBtn.classList.toggle('hidden'); markAllDoneBtn.classList.toggle('hidden'); todoListContainer.classList.toggle('hidden'); 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/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 48668562f09..a4778077bc4 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -1,94 +1,117 @@ <script> - import axios from '~/lib/utils/axios_utils'; +import axios from '~/lib/utils/axios_utils'; - import Flash from '~/flash'; - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; - import { n__, s__, sprintf } from '~/locale'; - import { redirectTo } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import Flash from '~/flash'; +import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { n__, s__, sprintf } from '~/locale'; +import { redirectTo } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - DeprecatedModal, +export default { + components: { + DeprecatedModal, + }, + props: { + issueCount: { + type: Number, + required: true, }, - props: { - issueCount: { - type: Number, - required: true, - }, - mergeRequestCount: { - type: Number, - required: true, - }, - milestoneId: { - type: Number, - required: true, - }, - milestoneTitle: { - type: String, - required: true, - }, - milestoneUrl: { - type: String, - required: true, - }, + mergeRequestCount: { + type: Number, + required: true, }, - computed: { - text() { - const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { milestoneTitle: this.milestoneTitle }); - - if (this.issueCount === 0 && this.mergeRequestCount === 0) { - return sprintf( - s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle}. -This milestone is not currently used in any issues or merge requests.`), - { - milestoneTitle, - }, - false, - ); - } + milestoneId: { + type: Number, + required: true, + }, + milestoneTitle: { + type: String, + required: true, + }, + milestoneUrl: { + type: String, + required: true, + }, + }, + computed: { + text() { + const milestoneTitle = sprintf('<strong>%{milestoneTitle}</strong>', { + milestoneTitle: this.milestoneTitle, + }); + if (this.issueCount === 0 && this.mergeRequestCount === 0) { return sprintf( s__(`Milestones| -You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. -Once deleted, it cannot be undone or recovered.`), +You’re about to permanently delete the milestone %{milestoneTitle}. +This milestone is not currently used in any issues or merge requests.`), { milestoneTitle, - issuesWithCount: n__('%d issue', '%d issues', this.issueCount), - mergeRequestsWithCount: n__('%d merge request', '%d merge requests', this.mergeRequestCount), }, false, ); - }, - title() { - return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { milestoneTitle: this.milestoneTitle }); - }, - }, - methods: { - onSubmit() { - eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); + } - return axios.delete(this.milestoneUrl) - .then((response) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true }); + return sprintf( + s__(`Milestones| +You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. +Once deleted, it cannot be undone or recovered.`), + { + milestoneTitle, + issuesWithCount: n__('%d issue', '%d issues', this.issueCount), + mergeRequestsWithCount: n__( + '%d merge request', + '%d merge requests', + this.mergeRequestCount, + ), + }, + false, + ); + }, + title() { + return sprintf(s__('Milestones|Delete milestone %{milestoneTitle}?'), { + milestoneTitle: this.milestoneTitle, + }); + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteMilestoneModal.requestStarted', this.milestoneUrl); - // follow the rediect to milestones overview page - redirectTo(response.request.responseURL); - }) - .catch((error) => { - eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false }); + return axios + .delete(this.milestoneUrl) + .then(response => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: true, + }); - if (error.response && error.response.status === 404) { - Flash(sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { milestoneTitle: this.milestoneTitle })); - } else { - Flash(sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { milestoneTitle: this.milestoneTitle })); - } - throw error; + // follow the rediect to milestones overview page + redirectTo(response.request.responseURL); + }) + .catch(error => { + eventHub.$emit('deleteMilestoneModal.requestFinished', { + milestoneUrl: this.milestoneUrl, + successful: false, }); - }, + + if (error.response && error.response.status === 404) { + Flash( + sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } else { + Flash( + sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), { + milestoneTitle: this.milestoneTitle, + }), + ); + } + throw error; + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index d51b5c221e3..1d559dc6e41 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -7,7 +7,9 @@ export default () => { Vue.use(Translate); const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); @@ -16,14 +18,16 @@ export default () => { button.querySelector('.js-loading-icon').classList.add('hidden'); }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); button.querySelector('.js-loading-icon').classList.remove('hidden'); eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneId: parseInt(button.dataset.milestoneId, 10), @@ -37,12 +41,12 @@ export default () => { }; const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('deleteMilestoneModal.mounted', () => { - deleteMilestoneButtons.forEach((button) => { + deleteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js index 8e79341e96a..fcc62a2b2af 100644 --- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -7,20 +7,24 @@ Vue.use(Translate); export default () => { const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (milestoneUrl) => { - const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`); + const onRequestStarted = milestoneUrl => { + const button = document.querySelector( + `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { milestoneTitle: button.dataset.milestoneTitle, @@ -32,12 +36,12 @@ export default () => { }; const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button'); - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteMilestoneModal.mounted', () => { - promoteMilestoneButtons.forEach((button) => { + promoteMilestoneButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index 04e50963699..883be18b336 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -3,9 +3,12 @@ import '~/profile/gl_crop'; import Profile from '~/profile/profile'; document.addEventListener('DOMContentLoaded', () => { - $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names + // eslint-disable-next-line func-names + $(document).on('input.ssh_key', '#key_key', function() { const $title = $('#key_title'); - const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); + const comment = $(this) + .val() + .match(/^\S+ \S+ (.+)\n?$/); // Extract the SSH Key title from its comment if (comment && comment.length > 1) { diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 8e8f47c21d8..417935e2ad0 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -5,7 +5,9 @@ document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; if (skippable) { - const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${ + twoFactorNode.dataset.two_factor_skip_url + }">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert .container-fluid'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index a9658fd1eb4..13ff47d53c2 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,6 +1,11 @@ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; -document.addEventListener('DOMContentLoaded', () => ( - new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)) -)); +document.addEventListener( + 'DOMContentLoaded', + () => + new NewBranchForm( + $('.js-create-branch-form'), + JSON.parse(document.getElementById('availableRefs').innerHTML), + ), +); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 80159a82bd4..3ccad513c05 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -31,14 +31,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartData = data => ({ labels: Object.keys(data), - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: _.values(data), - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: _.values(data), + }, + ], }); const hourData = chartData(projectChartData.hour); @@ -51,7 +53,9 @@ document.addEventListener('DOMContentLoaded', () => { responsiveChart($('#month-chart'), monthData); const data = projectChartData.languages; - const ctx = $('#languages-chart').get(0).getContext('2d'); + const ctx = $('#languages-chart') + .get(0) + .getContext('2d'); const options = { scaleOverlay: true, responsive: true, diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js index 71f629fbc13..f79c386b59e 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/index.js +++ b/app/assets/javascripts/pages/projects/graphs/show/index.js @@ -7,7 +7,8 @@ import ContributorsStatGraph from './stat_graph_contributors'; document.addEventListener('DOMContentLoaded', () => { const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath; - axios.get(url) + axios + .get(url) .then(({ data }) => { const graph = new ContributorsStatGraph(); graph.init(data); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 58bb8c5b0c8..76613394af6 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -3,7 +3,11 @@ import $ from 'jquery'; import _ from 'underscore'; import { n__, s__, createDateTimeFormat, sprintf } from '~/locale'; -import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; +import { + ContributorsGraph, + ContributorsAuthorGraph, + ContributorsMasterGraph, +} from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; export default (function() { @@ -14,7 +18,7 @@ export default (function() { ContributorsStatGraph.prototype.init = function(log) { var author_commits, total_commits; this.parsed_log = ContributorsStatGraphUtil.parse_log(log); - this.set_current_field("commits"); + this.set_current_field('commits'); total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); this.add_master_graph(total_commits); @@ -31,23 +35,26 @@ export default (function() { var limited_author_data; this.authors = []; limited_author_data = author_data.slice(0, 100); - return _.each(limited_author_data, (function(_this) { - return function(d) { - var author_graph, author_header; - author_header = _this.create_author_header(d); - $(".contributors-list").append(author_header); - - author_graph = new ContributorsAuthorGraph(d.dates); - _this.authors[d.author_name] = author_graph; - return author_graph.draw(); - }; - })(this)); + return _.each( + limited_author_data, + (function(_this) { + return function(d) { + var author_graph, author_header; + author_header = _this.create_author_header(d); + $('.contributors-list').append(author_header); + + author_graph = new ContributorsAuthorGraph(d.dates); + _this.authors[d.author_name] = author_graph; + return author_graph.draw(); + }; + })(this), + ); }; ContributorsStatGraph.prototype.format_author_commit_info = function(author) { var commits; commits = $('<span/>', { - "class": 'graph-author-commits-count' + class: 'graph-author-commits-count', }); commits.text(n__('%d commit', '%d commits', author.commits)); return $('<span/>').append(commits); @@ -56,13 +63,13 @@ export default (function() { ContributorsStatGraph.prototype.create_author_header = function(author) { var author_commit_info, author_commit_info_span, author_email, author_name, list_item; list_item = $('<li/>', { - "class": 'person', - style: 'display: block;' + class: 'person', + style: 'display: block;', }); author_name = $('<h4>' + author.author_name + '</h4>'); author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); author_commit_info_span = $('<span/>', { - "class": 'commits' + class: 'commits', }); author_commit_info = this.format_author_commit_info(author); author_commit_info_span.html(author_commit_info); @@ -80,37 +87,41 @@ export default (function() { }; ContributorsStatGraph.prototype.redraw_authors = function() { - $("ol").html(""); + $('ol').html(''); const { x_domain } = ContributorsGraph.prototype; - const author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); - - return _.each(author_commits, (function(_this) { - return function(d) { - _this.redraw_author_commit_info(d); - if (_this.authors[d.author_name] != null) { - $(_this.authors[d.author_name].list_item).appendTo("ol"); - _this.authors[d.author_name].set_data(d.dates); - return _this.authors[d.author_name].redraw(); - } - return ''; - }; - })(this)); + const author_commits = ContributorsStatGraphUtil.get_author_data( + this.parsed_log, + this.field, + x_domain, + ); + + return _.each( + author_commits, + (function(_this) { + return function(d) { + _this.redraw_author_commit_info(d); + if (_this.authors[d.author_name] != null) { + $(_this.authors[d.author_name].list_item).appendTo('ol'); + _this.authors[d.author_name].set_data(d.dates); + return _this.authors[d.author_name].redraw(); + } + return ''; + }; + })(this), + ); }; ContributorsStatGraph.prototype.set_current_field = function(field) { - return this.field = field; + return (this.field = field); }; ContributorsStatGraph.prototype.change_date_header = function() { const { x_domain } = ContributorsGraph.prototype; - const formattedDateRange = sprintf( - s__('ContributorsPage|%{startDate} – %{endDate}'), - { - startDate: this.dateFormat.format(new Date(x_domain[0])), - endDate: this.dateFormat.format(new Date(x_domain[1])), - }, - ); + const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), { + startDate: this.dateFormat.format(new Date(x_domain[0])), + endDate: this.dateFormat.format(new Date(x_domain[1])), + }); return $('#date_header').text(formattedDateRange); }; @@ -120,7 +131,7 @@ export default (function() { if ($author != null) { author_list_item = $(this.authors[author.author_name].list_item); author_commit_info = this.format_author_commit_info(author); - return author_list_item.find("span").html(author_commit_info); + return author_list_item.find('span').html(author_commit_info); } return ''; }; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 5f91686347a..377dce6c746 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -11,10 +11,32 @@ import { brushX } from 'd3-brush'; import { timeParse } from 'd3-time-format'; import { dateTickFormat } from '~/lib/utils/tick_formats'; -const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; +const d3 = { + extent, + max, + select, + scaleTime, + scaleLinear, + axisLeft, + axisBottom, + area, + brushX, + timeParse, +}; const hasProp = {}.hasOwnProperty; -const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; +const extend = function(child, parent) { + for (const key in parent) { + if (hasProp.call(parent, key)) child[key] = parent[key]; + } + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + child.__super__ = parent.prototype; + return child; +}; export const ContributorsGraph = (function() { function ContributorsGraph() {} @@ -23,7 +45,7 @@ export const ContributorsGraph = (function() { top: 20, right: 10, bottom: 30, - left: 40 + left: 40, }; ContributorsGraph.prototype.x_domain = null; @@ -33,35 +55,39 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { - const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const parentPaddingWidth = + parseFloat($parentElement.css('padding-left')) + + parseFloat($parentElement.css('padding-right')); const marginWidth = this.MARGIN.left + this.MARGIN.right; return baseWidth - parentPaddingWidth - marginWidth; }; ContributorsGraph.set_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = data; + return (ContributorsGraph.prototype.x_domain = data); }; ContributorsGraph.set_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_x_domain = function(data) { - return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { + return (ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) { return d.date; - }); + })); }; ContributorsGraph.init_y_domain = function(data) { - return ContributorsGraph.prototype.y_domain = [ - 0, d3.max(data, function(d) { - return d.commits = d.commits || d.additions || d.deletions; - }) - ]; + return (ContributorsGraph.prototype.y_domain = [ + 0, + d3.max(data, function(d) { + return (d.commits = d.commits || d.additions || d.deletions); + }), + ]); }; ContributorsGraph.init_domain = function(data) { @@ -70,7 +96,7 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.set_dates = function(data) { - return ContributorsGraph.prototype.dates = data; + return (ContributorsGraph.prototype.dates = data); }; ContributorsGraph.prototype.set_x_domain = function() { @@ -87,20 +113,33 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.prototype.create_scale = function(width, height) { - this.x = d3.scaleTime().range([0, width]).clamp(true); - return this.y = d3.scaleLinear().range([height, 0]).nice(); + this.x = d3 + .scaleTime() + .range([0, width]) + .clamp(true); + return (this.y = d3 + .scaleLinear() + .range([height, 0]) + .nice()); }; ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis); + return this.svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0, ' + this.height + ')') + .call(this.x_axis); }; ContributorsGraph.prototype.draw_y_axis = function() { - return this.svg.append("g").attr("class", "y axis").call(this.y_axis); + return this.svg + .append('g') + .attr('class', 'y axis') + .call(this.y_axis); }; ContributorsGraph.prototype.set_data = function(data) { - return this.data = data; + return (this.data = data); }; return ContributorsGraph; @@ -137,9 +176,9 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - const parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse('%Y-%m-%d'); return data.forEach(function(d) { - return d.date = parseDate(d.date); + return (d.date = parseDate(d.date)); }); }; @@ -148,42 +187,63 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsMasterGraph.prototype.create_svg = function() { - this.svg = d3.select("#contributors-master") - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "tint-box") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select('#contributors-master') + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'tint-box') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - return x(d.date); - }).y0(this.height).y1(function(d) { - d.commits = d.commits || d.additions || d.deletions; - return y(d.commits); - }); + return (this.area = d3 + .area() + .x(function(d) { + return x(d.date); + }) + .y0(this.height) + .y1(function(d) { + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); + })); }; ContributorsMasterGraph.prototype.create_brush = function() { - return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content); + return (this.brush = d3 + .brushX(this.x) + .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]) + .on('end', this.update_content)); }; ContributorsMasterGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area') + .attr('d', this.area); }; ContributorsMasterGraph.prototype.add_brush = function() { - return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height); + return this.svg + .append('g') + .attr('class', 'selection') + .call(this.brush) + .selectAll('rect') + .attr('height', this.height); }; ContributorsMasterGraph.prototype.update_content = function() { @@ -193,7 +253,7 @@ export const ContributorsMasterGraph = (function(superClass) { } else { ContributorsGraph.set_x_domain(this.x_max_domain); } - return $("#brush_change").trigger('change'); + return $('#brush_change').trigger('change'); }; ContributorsMasterGraph.prototype.draw = function() { @@ -216,9 +276,9 @@ export const ContributorsMasterGraph = (function(superClass) { this.process_dates(this.data); ContributorsGraph.set_y_domain(this.data); this.set_y_domain(); - this.svg.select("path").datum(this.data); - this.svg.select("path").attr("d", this.area); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.data); + this.svg.select('path').attr('d', this.area); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsMasterGraph; @@ -252,43 +312,58 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.axisBottom() + this.x_axis = d3 + .axisBottom() .scale(this.x) .ticks(8) .tickFormat(dateTickFormat); - return this.y_axis = d3.axisLeft().scale(this.y).ticks(5); + return (this.y_axis = d3 + .axisLeft() + .scale(this.y) + .ticks(5)); }; ContributorsAuthorGraph.prototype.create_area = function(x, y) { - return this.area = d3.area().x(function(d) { - const parseDate = d3.timeParse("%Y-%m-%d"); - return x(parseDate(d)); - }).y0(this.height).y1((function(_this) { - return function(d) { - if (_this.data[d] != null) { - return y(_this.data[d]); - } else { - return y(0); - } - }; - })(this)); + return (this.area = d3 + .area() + .x(function(d) { + const parseDate = d3.timeParse('%Y-%m-%d'); + return x(parseDate(d)); + }) + .y0(this.height) + .y1( + (function(_this) { + return function(d) { + if (_this.data[d] != null) { + return y(_this.data[d]); + } else { + return y(0); + } + }; + })(this), + )); }; ContributorsAuthorGraph.prototype.create_svg = function() { const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - this.svg = d3.select(this.list_item) - .append("svg") - .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) - .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) - .attr("class", "spark") - .append("g") - .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3 + .select(this.list_item) + .append('svg') + .attr('width', this.width + this.MARGIN.left + this.MARGIN.right) + .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr('class', 'spark') + .append('g') + .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { - return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area); + return this.svg + .append('path') + .datum(data) + .attr('class', 'area-contributor') + .attr('d', this.area); }; ContributorsAuthorGraph.prototype.draw = function() { @@ -304,10 +379,10 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.redraw = function() { this.set_domain(); - this.svg.select("path").datum(this.dates); - this.svg.select("path").attr("d", this.area); - this.svg.select(".x.axis").call(this.x_axis); - return this.svg.select(".y.axis").call(this.y_axis); + this.svg.select('path').datum(this.dates); + this.svg.select('path').attr('d', this.area); + this.svg.select('.x.axis').call(this.x_axis); + return this.svg.select('.y.axis').call(this.y_axis); }; return ContributorsAuthorGraph; diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index cd0e2bc023c..988ae164955 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -26,12 +26,12 @@ export default { by_author = _.toArray(by_author); return { total: total, - by_author: by_author + by_author: by_author, }; }, add_date: function(date, collection) { collection[date] = {}; - return collection[date].date = date; + return (collection[date].date = date); }, add_author: function(author, by_author, by_email) { var data, normalized_email; @@ -49,28 +49,28 @@ export default { return this.store_deletions(entry, total, by_author); }, store_commits: function(total, by_author) { - this.add(total, "commits", 1); - return this.add(by_author, "commits", 1); + this.add(total, 'commits', 1); + return this.add(by_author, 'commits', 1); }, add: function(collection, field, value) { if (collection[field] == null) { collection[field] = 0; } - return collection[field] += value; + return (collection[field] += value); }, store_additions: function(entry, total, by_author) { if (entry.additions == null) { entry.additions = 0; } - this.add(total, "additions", entry.additions); - return this.add(by_author, "additions", entry.additions); + this.add(total, 'additions', entry.additions); + return this.add(by_author, 'additions', entry.additions); }, store_deletions: function(entry, total, by_author) { if (entry.deletions == null) { entry.deletions = 0; } - this.add(total, "deletions", entry.deletions); - return this.add(by_author, "deletions", entry.deletions); + this.add(total, 'deletions', entry.deletions); + return this.add(by_author, 'deletions', entry.deletions); }, get_total_data: function(parsed_log, field) { var log, total_data; @@ -95,15 +95,18 @@ export default { } log = parsed_log.by_author; author_data = []; - _.each(log, (function(_this) { - return function(log_entry) { - var parsed_log_entry; - parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); - if (!_.isEmpty(parsed_log_entry.dates)) { - return author_data.push(parsed_log_entry); - } - }; - })(this)); + _.each( + log, + (function(_this) { + return function(log_entry) { + var parsed_log_entry; + parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range); + if (!_.isEmpty(parsed_log_entry.dates)) { + return author_data.push(parsed_log_entry); + } + }; + })(this), + ); return _.sortBy(author_data, function(d) { return d[field]; }).reverse(); @@ -120,16 +123,19 @@ export default { parsed_entry.additions = 0; parsed_entry.deletions = 0; - _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { - return function(value, key) { - if (_this.in_range(value.date, date_range)) { - parsed_entry.dates[value.date] = value[field]; - parsed_entry.commits += value.commits; - parsed_entry.additions += value.additions; - return parsed_entry.deletions += value.deletions; - } - }; - })(this)); + _.each( + _.omit(log_entry, 'author_name', 'author_email'), + (function(_this) { + return function(value, key) { + if (_this.in_range(value.date, date_range)) { + parsed_entry.dates[value.date] = value[field]; + parsed_entry.commits += value.commits; + parsed_entry.additions += value.additions; + return (parsed_entry.deletions += value.deletions); + } + }; + })(this), + ); return parsed_entry; }, in_range: function(date, date_range) { @@ -139,5 +145,5 @@ export default { } else { return false; } - } + }, }; diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index bc08ccf3584..bd8afa2d5ba 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -16,7 +16,8 @@ export default () => { ); const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); - const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); + const fileBlobPermalinkUrl = + fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 9f20a3e4e46..019efe077f7 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -1,7 +1,7 @@ import ZenMode from '~/zen_mode'; import GLForm from '~/gl_form'; -export default function ($formEl) { +export default function($formEl) { new ZenMode(); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index ef65196872c..8987c8e3f47 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -5,7 +5,7 @@ import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; -export default function () { +export default function() { initIssueableApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 5d2247f6c6d..e8b646f3f6e 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,72 +1,86 @@ <script> - import _ from 'underscore'; - import axios from '~/lib/utils/axios_utils'; - import createFlash from '~/flash'; - import GlModal from '~/vue_shared/components/gl_modal.vue'; - import { s__, sprintf } from '~/locale'; - import { visitUrl } from '~/lib/utils/url_utility'; - import eventHub from '../event_hub'; +import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import eventHub from '../event_hub'; - export default { - components: { - GlModal, +export default { + components: { + GlModal, + }, + props: { + url: { + type: String, + required: true, }, - props: { - url: { - type: String, - required: true, - }, - labelTitle: { - type: String, - required: true, - }, - labelColor: { - type: String, - required: true, - }, - labelTextColor: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + labelTitle: { + type: String, + required: true, }, - computed: { - text() { - return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. - Existing project labels with the same title will be merged. This action cannot be reversed.`), { + labelColor: { + type: String, + required: true, + }, + labelTextColor: { + type: String, + required: true, + }, + groupName: { + type: String, + required: true, + }, + }, + computed: { + text() { + return sprintf( + s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. + Existing project labels with the same title will be merged. This action cannot be reversed.`), + { labelTitle: this.labelTitle, groupName: this.groupName, - }); - }, - title() { - const label = `<span + }, + ); + }, + title() { + const label = `<span class="label color-label" style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" >${_.escape(this.labelTitle)}</span>`; - return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), { + return sprintf( + s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), + { labelTitle: label, - }, false); - }, + }, + false, + ); }, - methods: { - onSubmit() { - eventHub.$emit('promoteLabelModal.requestStarted', this.url); - return axios.post(this.url, { params: { format: 'json' } }) - .then((response) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true }); - visitUrl(response.data.url); - }) - .catch((error) => { - eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false }); - createFlash(error); + }, + methods: { + onSubmit() { + eventHub.$emit('promoteLabelModal.requestStarted', this.url); + return axios + .post(this.url, { params: { format: 'json' } }) + .then(response => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: true, + }); + visitUrl(response.data.url); + }) + .catch(error => { + eventHub.$emit('promoteLabelModal.requestFinished', { + labelUrl: this.url, + successful: false, }); - }, + createFlash(error); + }); }, - }; + }, +}; </script> <template> <gl-modal diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index 03cfef61311..36cf485f33d 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -10,20 +10,24 @@ const initLabelIndex = () => { initLabels(); const onRequestFinished = ({ labelUrl, successful }) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); if (!successful) { button.removeAttribute('disabled'); } }; - const onRequestStarted = (labelUrl) => { - const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`); + const onRequestStarted = labelUrl => { + const button = document.querySelector( + `.js-promote-project-label-button[data-url="${labelUrl}"]`, + ); button.setAttribute('disabled', ''); eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished); }; - const onDeleteButtonClick = (event) => { + const onDeleteButtonClick = event => { const button = event.currentTarget; const modalProps = { labelTitle: button.dataset.labelTitle, @@ -37,12 +41,12 @@ const initLabelIndex = () => { }; const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button'); - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.addEventListener('click', onDeleteButtonClick); }); eventHub.$once('promoteLabelModal.mounted', () => { - promoteLabelButtons.forEach((button) => { + promoteLabelButtons.forEach(button => { button.removeAttribute('disabled'); }); }); diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js index 70fbb3f301c..226d63f05c4 100644 --- a/app/assets/javascripts/pages/projects/network/network.js +++ b/app/assets/javascripts/pages/projects/network/network.js @@ -6,13 +6,15 @@ import BranchGraph from '../../../network/branch_graph'; export default (function() { function Network(opts) { var vph; - $("#filter_ref").click(function() { - return $(this).closest('form').submit(); + $('#filter_ref').click(function() { + return $(this) + .closest('form') + .submit(); }); - this.branch_graph = new BranchGraph($(".network-graph"), opts); + this.branch_graph = new BranchGraph($('.network-graph'), opts); vph = $(window).height() - 250; $('.network-graph').css({ - 'height': vph + 'px' + height: vph + 'px', }); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index 544360dcd51..6197dc8a9db 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,12 +1,16 @@ import Vue from 'vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipeline-schedules-callout', - components: { - 'pipeline-schedules-callout': PipelineSchedulesCallout, - }, - render(createElement) { - return createElement('pipeline-schedules-callout'); - }, -})); +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipeline-schedules-callout', + components: { + 'pipeline-schedules-callout': PipelineSchedulesCallout, + }, + render(createElement) { + return createElement('pipeline-schedules-callout'); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index ef53d67e7cb..ab6f42d928c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,63 +1,63 @@ <script> - import _ from 'underscore'; +import _ from 'underscore'; - export default { - props: { - initialCronInterval: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - inputNameAttribute: 'schedule[cron]', - cronInterval: this.initialCronInterval, - cronIntervalPresets: { - everyDay: '0 4 * * *', - everyWeek: '0 4 * * 0', - everyMonth: '0 4 1 * *', - }, - cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', - customInputEnabled: false, - }; +export default { + props: { + initialCronInterval: { + type: String, + required: false, + default: '', }, - computed: { - intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); - }, - // The text input is editable when there's a custom interval, or when it's - // a preset interval and the user clicks the 'custom' radio button - isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); + }, + data() { + return { + inputNameAttribute: 'schedule[cron]', + cronInterval: this.initialCronInterval, + cronIntervalPresets: { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', }, + cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', + customInputEnabled: false, + }; + }, + computed: { + intervalIsPreset() { + return _.contains(this.cronIntervalPresets, this.cronInterval); }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, + // The text input is editable when there's a custom interval, or when it's + // a preset interval and the user clicks the 'custom' radio button + isEditable() { + return !!(this.customInputEnabled || !this.intervalIsPreset); }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } + }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); }, - methods: { - toggleCustomInput(shouldEnable) { - this.customInputEnabled = shouldEnable; + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, + methods: { + toggleCustomInput(shouldEnable) { + this.customInputEnabled = shouldEnable; - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } - }, + if (shouldEnable) { + // We need to change the value so other radios don't remain selected + // because the model (cronInterval) hasn't changed. The server trims it. + this.cronInterval = `${this.cronInterval} `; + } }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index 77508e62cef..33fc2420e4d 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,31 +1,31 @@ <script> - import Vue from 'vue'; - import Cookies from 'js-cookie'; - import Translate from '../../../../../vue_shared/translate'; - import illustrationSvg from '../icons/intro_illustration.svg'; +import Vue from 'vue'; +import Cookies from 'js-cookie'; +import Translate from '../../../../../vue_shared/translate'; +import illustrationSvg from '../icons/intro_illustration.svg'; - Vue.use(Translate); +Vue.use(Translate); - const cookieKey = 'pipeline_schedules_callout_dismissed'; +const cookieKey = 'pipeline_schedules_callout_dismissed'; - export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; +export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, - created() { - this.illustrationSvg = illustrationSvg; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js index 4ef0d11dd36..0057700c1b3 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js @@ -26,8 +26,7 @@ export default class TargetBranchDropdown { } formatBranchesList() { - return this.$dropdown.data('data') - .map(val => ({ name: val })); + return this.$dropdown.data('data').map(val => ({ name: val })); } setDropdownToggle() { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index c3ac54733a3..4d494efef6c 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -11,7 +11,9 @@ Vue.use(Translate); function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); - const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; + const initialCronInterval = intervalPatternMount + ? intervalPatternMount.dataset.initialInterval + : ''; return new Vue({ el: intervalPatternMount, diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index 07b6992eba1..48353f3b4ef 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -7,26 +7,29 @@ const options = { maintainAspectRatio: false, }; -const buildChart = (chartScope) => { +const buildChart = chartScope => { const data = { labels: chartScope.labels, - datasets: [{ - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, - }, - { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, - }, + datasets: [ + { + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, ], }; - const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + const ctx = $(`#${chartScope.scope}Chart`) + .get(0) + .getContext('2d'); new Chart(ctx).Line(data, options); }; @@ -36,14 +39,16 @@ document.addEventListener('DOMContentLoaded', () => { const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); const data = { labels: chartTimesData.labels, - datasets: [{ - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, - data: chartTimesData.values, - }], + datasets: [ + { + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartTimesData.values, + }, + ], }; if (window.innerWidth < 768) { @@ -51,7 +56,11 @@ document.addEventListener('DOMContentLoaded', () => { options.scaleFontSize = 8; } - new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options); + new Chart( + $('#build_timesChart') + .get(0) + .getContext('2d'), + ).Bar(data, options); chartsData.forEach(scope => buildChart(scope)); }); diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index a84e2790680..fc337a7609b 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -6,35 +6,39 @@ import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#pipelines-list-vue', - components: { - pipelinesComponent, - }, - data() { - return { - store: new PipelinesStore(), - }; - }, - created() { - this.dataset = document.querySelector(this.$options.el).dataset; - }, - render(createElement) { - return createElement('pipelines-component', { - props: { - store: this.store, - endpoint: this.dataset.endpoint, - helpPagePath: this.dataset.helpPagePath, - emptyStateSvgPath: this.dataset.emptyStateSvgPath, - errorStateSvgPath: this.dataset.errorStateSvgPath, - noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, - autoDevopsPath: this.dataset.helpAutoDevopsPath, - newPipelinePath: this.dataset.newPipelinePath, - canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), - hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), - ciLintPath: this.dataset.ciLintPath, - resetCachePath: this.dataset.resetCachePath, +document.addEventListener( + 'DOMContentLoaded', + () => + new Vue({ + el: '#pipelines-list-vue', + components: { + pipelinesComponent, }, - }); - }, -})); + data() { + return { + store: new PipelinesStore(), + }; + }, + created() { + this.dataset = document.querySelector(this.$options.el).dataset; + }, + render(createElement) { + return createElement('pipelines-component', { + props: { + store: this.store, + endpoint: this.dataset.endpoint, + helpPagePath: this.dataset.helpPagePath, + emptyStateSvgPath: this.dataset.emptyStateSvgPath, + errorStateSvgPath: this.dataset.errorStateSvgPath, + noPipelinesSvgPath: this.dataset.noPipelinesSvgPath, + autoDevopsPath: this.dataset.helpAutoDevopsPath, + newPipelinePath: this.dataset.newPipelinePath, + canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline), + hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi), + ciLintPath: this.dataset.ciLintPath, + resetCachePath: this.dataset.resetCachePath, + }, + }); + }, + }), +); diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js index 94dfeb96e8c..ba4ae04ab3d 100644 --- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -2,9 +2,12 @@ import Pipelines from '~/pipelines'; export default () => { const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; + const pipelineStatusUrl = `${document + .querySelector('.js-pipeline-tab-link a') + .getAttribute('href')}/status.json`; - new Pipelines({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Pipelines({ initTabs: true, pipelineStatusUrl, tabsOptions: { 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/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 06101290f6c..dced839c883 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -1,73 +1,71 @@ <script> - import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; +import projectFeatureToggle from '../../../../../vue_shared/components/toggle_button.vue'; - export default { - components: { - projectFeatureToggle, - }, +export default { + components: { + projectFeatureToggle, + }, - model: { - prop: 'value', - event: 'change', - }, + model: { + prop: 'value', + event: 'change', + }, - props: { - name: { - type: String, - required: false, - default: '', - }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + }, - computed: { - featureEnabled() { - return this.value !== 0; - }, + computed: { + featureEnabled() { + return this.value !== 0; + }, - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; - }, + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [[0, 'Enable feature to choose access level']]; + }, - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; }, + }, - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } - }, + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, - selectOption(e) { - this.$emit('change', Number(e.target.value)); - }, + selectOption(e) { + this.$emit('change', Number(e.target.value)); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 83437363af5..898d605463f 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -1,23 +1,23 @@ <script> - export default { - props: { - label: { - type: String, - required: false, - default: null, - }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, +export default { + props: { + label: { + type: String, + required: false, + default: null, }, - }; + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js index ce47562f259..bc5c29d12b5 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js @@ -5,7 +5,9 @@ export const visibilityOptions = { }; export const visibilityLevelDescriptions = { - [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', + [visibilityOptions.PRIVATE]: + 'The project is accessible only by members of the project. Access must be granted explicitly to each user.', [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.', - [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.', + [visibilityOptions.PUBLIC]: + 'The project can be accessed by anyone, regardless of authentication.', }; diff --git a/app/assets/javascripts/pages/projects/shared/project_avatar.js b/app/assets/javascripts/pages/projects/shared/project_avatar.js index 447877752fe..1e69ecb481d 100644 --- a/app/assets/javascripts/pages/projects/shared/project_avatar.js +++ b/app/assets/javascripts/pages/projects/shared/project_avatar.js @@ -8,8 +8,9 @@ export default function projectAvatar() { $('.js-project-avatar-input').bind('change', function onClickAvatarInput() { const form = $(this).closest('form'); - // eslint-disable-next-line no-useless-escape - const filename = $(this).val().replace(/^.*[\\\/]/, ''); + const filename = $(this) + .val() + .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape return form.find('.js-avatar-filename').text(filename); }); } diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 75cb6374ad5..f970a5ebb64 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -1,8 +1,15 @@ <script> import _ from 'underscore'; import { s__, sprintf } from '~/locale'; +import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui'; export default { + components: { + GlModal, + }, + directives: { + 'gl-modal': GlModalDirective, + }, props: { deleteWikiUrl: { type: String, @@ -54,7 +61,7 @@ export default { > {{ __('Delete') }} </button> - <gl-ui-modal + <gl-modal :title="title" :ok-title="s__('WikiPageConfirmDelete|Delete page')" :modal-id="modalId" @@ -81,6 +88,6 @@ export default { name="authenticity_token" /> </form> - </gl-ui-modal> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index c2629090f01..f5fd84d69ac 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -21,7 +21,8 @@ document.addEventListener('DOMContentLoaded', () => { const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; - new Vue({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Vue({ el: deleteWikiModalWrapperEl, data: { deleteWikiUrl: '', diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index e3e0ab91993..0c896c8599e 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -22,7 +22,7 @@ export default class Search { fields: ['full_name'], }, data(term, callback) { - return Api.groups(term, {}, (data) => { + return Api.groups(term, {}, data => { data.unshift({ full_name: 'Any', }); @@ -37,7 +37,7 @@ export default class Search { return obj.full_name; }, toggleLabel(obj) { - return `${($groupDropdown.data('defaultLabel'))} ${obj.full_name}`; + return `${$groupDropdown.data('defaultLabel')} ${obj.full_name}`; }, clicked: () => Search.submitSearch(), }); @@ -52,7 +52,7 @@ export default class Search { }, data: (term, callback) => { this.getProjectsData(term) - .then((data) => { + .then(data => { data.unshift({ name_with_namespace: 'Any', }); @@ -70,7 +70,7 @@ export default class Search { return obj.name_with_namespace; }, toggleLabel(obj) { - return `${($projectDropdown.data('defaultLabel'))} ${obj.name_with_namespace}`; + return `${$projectDropdown.data('defaultLabel')} ${obj.name_with_namespace}`; }, clicked: () => Search.submitSearch(), }); @@ -99,17 +99,24 @@ export default class Search { } clearSearchField() { - return $(this.searchInput).val('').trigger('keyup').focus(); + return $(this.searchInput) + .val('') + .trigger('keyup') + .focus(); } getProjectsData(term) { - return new Promise((resolve) => { + return new Promise(resolve => { if (this.groupId) { Api.groupProjects(this.groupId, term, {}, resolve); } else { - Api.projects(term, { - order_by: 'id', - }, resolve); + Api.projects( + term, + { + order_by: 'id', + }, + resolve, + ); } }); } diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 1e7c29aefaa..2b8f1e8b0ef 100644 --- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -20,7 +20,7 @@ export default class SigninTabsMemoizer { bootstrap() { const tabs = document.querySelectorAll(this.tabSelector); if (tabs.length > 0) { - tabs[0].addEventListener('click', (e) => { + tabs[0].addEventListener('click', e => { if (e.target && e.target.nodeName === 'A') { const anchorName = e.target.getAttribute('href'); this.saveData(anchorName); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index d621f988d86..7a41805bada 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -22,10 +22,10 @@ export default class UsernameValidator { available: false, valid: false, pending: false, - empty: true + empty: true, }; - const debounceTimeout = _.debounce((username) => { + const debounceTimeout = _.debounce(username => { this.validateUsername(username); }, debounceTimeoutDuration); @@ -81,7 +81,8 @@ export default class UsernameValidator { this.state.pending = true; this.state.available = false; this.renderState(); - axios.get(`${gon.relative_url_root}/users/${username}/exists`) + axios + .get(`${gon.relative_url_root}/users/${username}/exists`) .then(({ data }) => this.setAvailabilityState(data.exists)) .catch(() => flash(__('An error occurred while validating username'))); } @@ -100,8 +101,7 @@ export default class UsernameValidator { clearFieldValidationState() { this.inputElement.siblings('p').hide(); - this.inputElement.removeClass(invalidInputClass) - .removeClass(successInputClass); + this.inputElement.removeClass(invalidInputClass).removeClass(successInputClass); } setUnavailableState() { diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index 6b1626b0161..a191df00dfa 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -13,10 +13,12 @@ function initUserProfile(action) { new UserTabs({ parentEl: '.user-profile', action }); // hide project limit message - $('.hide-project-limit-message').on('click', (e) => { + $('.hide-project-limit-message').on('click', e => { e.preventDefault(); Cookies.set('hide_project_limit_message', 'false'); - $(this).parents('.project-limit-message').remove(); + $(this) + .parents('.project-limit-message') + .remove(); }); } 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/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 16e69759091..a7507fb3b6f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,16 +1,17 @@ <script> import { s__, sprintf } from '~/locale'; -import { formatTime } from '~/lib/utils/datetime_utility'; import eventHub from '../event_hub'; -import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; export default { directives: { tooltip, }, components: { - icon, + Icon, + GlCountdown, }, props: { actions: { @@ -51,11 +52,6 @@ export default { return !action.playable; }, - - remainingTime(action) { - const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now(); - return formatTime(Math.max(0, remainingMilliseconds)); - }, }, }; </script> @@ -100,7 +96,7 @@ export default { class="pull-right" > <icon name="clock" /> - {{ remainingTime(action) }} + <gl-countdown :end-date-string="action.scheduled_at" /> </span> </button> </li> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index d40de95e051..e0f0434e03d 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,13 +1,13 @@ <script> import tooltip from '../../vue_shared/directives/tooltip'; -import icon from '../../vue_shared/components/icon.vue'; +import Icon from '../../vue_shared/components/icon.vue'; export default { directives: { tooltip, }, components: { - icon, + Icon, }, props: { artifacts: { diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index cb14d4400d1..3339b5c13ed 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -88,25 +88,25 @@ export default { class="table-section section-10 js-pipeline-status pipeline-status" role="rowheader" > - Status + {{ s__('Pipeline|Status') }} </div> <div class="table-section section-15 js-pipeline-info pipeline-info" role="rowheader" > - Pipeline + {{ s__('Pipeline|Pipeline') }} </div> <div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader" > - Commit + {{ s__('Pipeline|Commit') }} </div> <div class="table-section section-20 js-pipeline-stages pipeline-stages" role="rowheader" > - Stages + {{ s__('Pipeline|Stages') }} </div> </div> <pipelines-table-row-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4f89ee66023..026d533d10f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -261,7 +261,7 @@ export default { class="table-mobile-header" role="rowheader" > - Status + {{ s__('Pipeline|Status') }} </div> <div class="table-mobile-content"> <ci-badge @@ -279,8 +279,9 @@ export default { <div class="table-section section-20"> <div class="table-mobile-header" - role="rowheader"> - Commit + role="rowheader" + > + {{ s__('Pipeline|Commit') }} </div> <div class="table-mobile-content"> <commit-component @@ -298,8 +299,9 @@ export default { <div class="table-section section-wrap section-20 stage-cell"> <div class="table-mobile-header" - role="rowheader"> - Stages + role="rowheader" + > + {{ s__('Pipeline|Stages') }} </div> <div class="table-mobile-content"> <template v-if="pipeline.details.stages.length > 0"> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index cd43d78de40..bed690200b8 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -60,7 +60,7 @@ export default { class="table-mobile-header" role="rowheader" > - Duration + {{ s__('Pipeline|Duration') }} </div> <div class="table-mobile-content"> <p @@ -87,7 +87,8 @@ export default { v-tooltip :title="tooltipTitle(finishedTime)" data-placement="top" - data-container="body"> + data-container="body" + > {{ timeFormated(finishedTime) }} </time> </p> 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/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 7bde4860973..ad4f5320ff8 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -226,7 +226,7 @@ export class SearchAutocomplete { icon, text: term, template: s__('SearchAutocomplete|in all GitLab'), - url: `/search?search=${term}`, + url: `${gon.relative_url_root}/search?search=${term}`, }); if (template) { @@ -234,7 +234,7 @@ export class SearchAutocomplete { icon, text: term, template, - url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, + url: `${gon.relative_url_root}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, }); } } diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index 43f0b6651b9..8950ae31627 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import GfmAutoComplete from '~/gfm_auto_complete'; import { __, s__ } from '~/locale'; import Api from '~/api'; +import { GlModal } from '@gitlab-org/gitlab-ui'; import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; @@ -13,6 +14,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu'; export default { components: { Icon, + GlModal, }, props: { currentEmoji: { @@ -152,7 +154,7 @@ export default { </script> <template> - <gl-ui-modal + <gl-modal :title="s__('SetStatusModal|Set a status')" :modal-id="modalId" :ok-title="s__('SetStatusModal|Set status')" @@ -237,5 +239,5 @@ export default { </div> </div> </div> - </gl-ui-modal> + </gl-modal> </template> 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..57c52a2016a 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'; @@ -9,6 +10,7 @@ import { visitUrl } from '../../lib/utils/url_utility'; import createFlash from '../../flash'; import MemoryUsage from './memory_usage.vue'; import StatusIcon from './mr_widget_status_icon.vue'; +import ReviewAppLink from './review_app_link.vue'; import MRWidgetService from '../services/mr_widget_service'; export default { @@ -20,6 +22,7 @@ export default { Icon, TooltipOnTruncate, FilteredSearchDropdown, + ReviewAppLink, }, directives: { tooltip, @@ -31,6 +34,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 +62,19 @@ export default { hasMetrics() { return !!this.deployment.metrics_url; }, + deployedText() { + return this.$options.deployedTextMap[this.deployment.status]; + }, + shouldRenderDropdown() { + return ( + this.enableCiEnvironmentsStatusChanges && + (this.deployment.changes && this.deployment.changes.length > 0) + ); + }, }, 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 +104,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" @@ -124,7 +141,7 @@ export default { <div> <template v-if="hasExternalUrls"> <filtered-search-dropdown - v-if="enableCiEnvironmentsStatusChanges" + v-if="shouldRenderDropdown" class="js-mr-wigdet-deployment-dropdown inline" :items="deployment.changes" :main-action-link="deployment.external_url" @@ -134,18 +151,10 @@ export default { slot="mainAction" slot-scope="slotProps" > - <a - :href="deployment.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="deploy-link js-deploy-url inline" - :class="slotProps.className" - > - <span> - {{ __('View app') }} - <icon name="external-link" /> - </span> - </a> + <review-app-link + :link="deployment.external_url" + :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" + /> </template> <template @@ -168,18 +177,11 @@ export default { </a> </template> </filtered-search-dropdown> - <a + <review-app-link v-else - :href="deployment.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline" - > - <span> - {{ __('View app') }} - <icon name="external-link" /> - </span> - </a> + :link="deployment.external_url" + css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" + /> </template> <loading-button v-if="deployment.stop_url" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index fee41b239e8..bd1946f337e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable vue/require-default-prop */ +import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -36,6 +37,10 @@ export default { type: String, required: false, }, + troubleshootingDocsPath: { + type: String, + required: true, + }, }, computed: { hasPipeline() { @@ -57,6 +62,12 @@ export default { hasCommitInfo() { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, + errorText() { + return sprintf(__('Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}'), { + linkStart: `<a href="${this.troubleshootingDocsPath}">`, + linkEnd: '</a>', + }); + }, }, }; </script> @@ -77,8 +88,10 @@ export default { name="status_failed_borderless" /> </div> - <div class="media-body"> - Could not connect to the CI server. Please check your settings and try again + <div + class="media-body" + v-html="errorText" + > </div> </template> <template v-else-if="hasPipeline"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue new file mode 100644 index 00000000000..b007d4f4dcb --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue @@ -0,0 +1,30 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + link: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <a + :href="link" + target="_blank" + rel="noopener noreferrer nofollow" + :class="cssClass" + > + {{ __('View app') }} + <icon name="external-link" /> + </a> +</template> 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..33064f82ef7 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; @@ -266,10 +301,12 @@ export default { :has-ci="mr.hasCI" :source-branch="mr.sourceBranch" :source-branch-link="mr.sourceBranchLink" + :troubleshooting-docs-path="mr.troubleshootingDocsPath" /> <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 +347,23 @@ 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" + :troubleshooting-docs-path="mr.troubleshootingDocsPath" + /> + <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..c17e9c9e4d6 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'; @@ -18,6 +18,8 @@ export default class MergeRequestStore { this.squash = data.squash; this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path; + this.troubleshootingDocsPath = + this.troubleshootingDocsPath || data.troubleshooting_docs_path; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.iid = data.iid; @@ -32,7 +34,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/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue new file mode 100644 index 00000000000..9327a2a4a6c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue @@ -0,0 +1,49 @@ +<script> +import { calculateRemainingMilliseconds, formatTime } from '~/lib/utils/datetime_utility'; + +/** + * Counts down to a given end date. + */ +export default { + props: { + endDateString: { + type: String, + required: true, + validator(value) { + return !Number.isNaN(new Date(value).getTime()); + }, + }, + }, + + data() { + return { + remainingTime: formatTime(0), + countdownUpdateIntervalId: null, + }; + }, + + mounted() { + const updateRemainingTime = () => { + const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString); + this.remainingTime = formatTime(remainingMilliseconds); + }; + + updateRemainingTime(); + this.countdownUpdateIntervalId = window.setInterval(updateRemainingTime, 1000); + }, + + beforeDestroy() { + window.clearInterval(this.countdownUpdateIntervalId); + }, +}; +</script> + +<template> + <time + v-gl-tooltip + :datetime="endDateString" + :title="endDateString" + > + {{ remainingTime }} + </time> +</template> 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/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue index 1f2a679c145..89dcf049f6e 100644 --- a/app/assets/javascripts/vue_shared/components/pagination_links.vue +++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue @@ -1,7 +1,11 @@ <script> +import { GlPagination } from '@gitlab-org/gitlab-ui'; import { s__ } from '../../locale'; export default { + components: { + GlPagination, + }, props: { change: { type: Function, 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/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 4f2412ce520..549d27e96d9 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -9,6 +9,14 @@ export default { componentUpdated(el) { $(el).tooltip('_fixTitle'); + + // update visible tooltips + const tooltipInstance = $(el).data('bs.tooltip'); + const tip = tooltipInstance.getTipElement(); + tooltipInstance.setElementContent( + $(tip.querySelectorAll('.tooltip-inner')), + tooltipInstance.getTitle(), + ); }, unbind(el) { 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/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 6d891e21556..e261bd7c0ca 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -34,7 +34,7 @@ margin-bottom: 0; } - *:first-child:not(.katex-display) { + *:first-child { margin-top: 0; } 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/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8040a14ef56..8f683ca06ad 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -61,7 +61,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController format.html do usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data) - render html: Gitlab::Highlight.highlight('payload.json', usage_data_json) + render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json') end format.json { render json: Gitlab::UsageData.to_json } end 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/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index b7e4f9b81f1..3cdf4ddf8bb 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -50,7 +50,10 @@ module BoardsResponses end def authorize_create_issue - authorize_action_for!(project, :admin_issue) + list = List.find(issue_params[:list_id]) + action = list.backlog? ? :create_issue : :admin_issue + + authorize_action_for!(project, action) end def authorize_admin_list 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/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 8d259b4052e..cdc6f53df8e 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -5,6 +5,7 @@ class Groups::BoardsController < Groups::ApplicationController before_action :assign_endpoint_vars before_action :boards, only: :index + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -13,6 +14,9 @@ class Groups::BoardsController < Groups::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.group, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -31,4 +35,18 @@ class Groups::BoardsController < Groups::ApplicationController def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(group, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(group_board_path(id: recently_visited.board_id), status: :found) + end + 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/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 56a884b8a2a..c02ec407262 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController apply_diff_view_cookie! @blob.load_all_data! - @lines = Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: @repository).lines + @lines = @blob.present.highlight.lines @form = UnfoldForm.new(params) diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 77b818347c7..8189b5d182a 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -8,6 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :authorize_read_board!, only: [:index, :show] before_action :boards, only: :index before_action :assign_endpoint_vars + before_action :redirect_to_recent_board, only: :index def index respond_with_boards @@ -16,6 +17,9 @@ class Projects::BoardsController < Projects::ApplicationController def show @board = boards.find(params[:id]) + # add/update the board in the recent visited table + Boards::Visits::CreateService.new(@board.project, current_user).execute(@board) if request.format.html? + respond_with_board end @@ -33,10 +37,24 @@ class Projects::BoardsController < Projects::ApplicationController end def authorize_read_board! - return access_denied! unless can?(current_user, :read_board, project) + access_denied! unless can?(current_user, :read_board, project) end def serialize_as_json(resource) resource.as_json(only: [:id]) end + + def includes_board?(board_id) + boards.any? { |board| board.id == board_id } + end + + def redirect_to_recent_board + return if request.format.json? + + recently_visited = Boards::Visits::LatestService.new(project, current_user).execute + + if recently_visited && includes_board?(recently_visited.board_id) + redirect_to(namespace_project_board_path(id: recently_visited.board_id), status: :found) + end + end end 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/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb index 78d5faf2326..53176978416 100644 --- a/app/controllers/projects/mirrors_controller.rb +++ b/app/controllers/projects/mirrors_controller.rb @@ -44,6 +44,22 @@ class Projects::MirrorsController < Projects::ApplicationController redirect_to_repository_settings(project, anchor: 'js-push-remote-settings') end + def ssh_host_keys + lookup = SshHostKey.new(project: project, url: params[:ssh_url], compare_host_keys: params[:compare_host_keys]) + + if lookup.error.present? + # Failed to read keys + render json: { message: lookup.error }, status: :bad_request + elsif lookup.known_hosts.nil? + # Still working, come back later + render body: nil, status: :no_content + else + render json: lookup + end + rescue ArgumentError => err + render json: { message: err.message }, status: :bad_request + end + private def remote_mirror 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/issuable_finder.rb b/app/finders/issuable_finder.rb index 8abfe0c4c17..eb3d2498830 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -14,7 +14,7 @@ # project_id: integer # milestone_title: string # author_id: integer -# assignee_id: integer +# assignee_id: integer or 'None' or 'Any' # search: string # label_name: string # sort: string @@ -34,6 +34,11 @@ class IssuableFinder requires_cross_project_access unless: -> { project? } + # This is used as a common filter for None / Any + FILTER_NONE = 'none'.freeze + FILTER_ANY = 'any'.freeze + + # This is accepted as a deprecated filter and is also used in unassigning users NONE = '0'.freeze attr_accessor :current_user, :params @@ -236,16 +241,20 @@ class IssuableFinder # rubocop: enable CodeReuse/ActiveRecord def assignee_id? - params[:assignee_id].present? && params[:assignee_id].to_s != NONE + params[:assignee_id].present? end def assignee_username? - params[:assignee_username].present? && params[:assignee_username].to_s != NONE + params[:assignee_username].present? end - def no_assignee? + def filter_by_no_assignee? # Assignee_id takes precedence over assignee_username - params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == NONE + [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE + end + + def filter_by_any_assignee? + params[:assignee_id].to_s.downcase == FILTER_ANY end # rubocop: disable CodeReuse/ActiveRecord @@ -399,15 +408,17 @@ class IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items = items.where(assignee_id: assignee.id) - elsif no_assignee? - items = items.where(assignee_id: nil) + if filter_by_no_assignee? + items.where(assignee_id: nil) + elsif filter_by_any_assignee? + items.where('assignee_id IS NOT NULL') + elsif assignee + items.where(assignee_id: assignee.id) elsif assignee_id? || assignee_username? # assignee not found - items = items.none + items.none + else + items end - - items end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index abdc47b9866..cee57a83df4 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -137,10 +137,12 @@ class IssuesFinder < IssuableFinder # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) - if assignee - items.assigned_to(assignee) - elsif no_assignee? + if filter_by_no_assignee? items.unassigned + elsif filter_by_any_assignee? + items.assigned + elsif assignee + items.assigned_to(assignee) elsif assignee_id? || assignee_username? # assignee not found items.none else 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/blob_helper.rb b/app/helpers/blob_helper.rb index ff7f1e3a9aa..638744a1426 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module BlobHelper - def highlight(blob_name, blob_content, repository: nil, plain: false) - plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE - highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository) + def highlight(file_name, file_content, language: nil, plain: false) + highlighted = Gitlab::Highlight.highlight(file_name, file_content, plain: plain, language: language) raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>) end 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/blob.rb b/app/models/blob.rb index 31a839274b5..4f310e70f4f 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects +# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob class Blob < SimpleDelegator + include Presentable + include BlobLanguageFromGitAttributes + CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour - MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte - # Finding a viewer for a blob happens based only on extension and whether the # blob is binary or text, which means 1 blob should only be matched by 1 viewer, # and the order of these viewers doesn't really matter. @@ -121,10 +122,6 @@ class Blob < SimpleDelegator end end - def no_highlighting? - raw_size && raw_size > MAXIMUM_TEXT_HIGHLIGHT_SIZE - end - def empty? raw_size == 0 end diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb new file mode 100644 index 00000000000..92abbb67222 --- /dev/null +++ b/app/models/board_group_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific group a user has visited +class BoardGroupRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :group + belongs_to :board + + validates :user, presence: true + validates :group, presence: true + validates :board, presence: true + + scope :by_user_group, -> (user, group) { where(user: user, group: group).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, group: board.group, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, group) + by_user_group(user, group).last + end +end diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb new file mode 100644 index 00000000000..7cffff906d8 --- /dev/null +++ b/app/models/board_project_recent_visit.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Tracks which boards in a specific project a user has visited +class BoardProjectRecentVisit < ActiveRecord::Base + belongs_to :user + belongs_to :project + belongs_to :board + + validates :user, presence: true + validates :project, presence: true + validates :board, presence: true + + scope :by_user_project, -> (user, project) { where(user: user, project: project).order(:updated_at) } + + def self.visited!(user, board) + visit = find_or_create_by(user: user, project: board.project, board: board) + visit.touch if visit.updated_at < Time.now + rescue ActiveRecord::RecordNotUnique + retry + end + + def self.latest(user, project) + by_user_project(user, project).last + end +end 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 06507345fe8..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 } ## @@ -109,7 +110,7 @@ class CommitStatus < ActiveRecord::Base before_transition any => :failed do |commit_status, transition| failure_reason = transition.args.first - commit_status.failure_reason = failure_reason + commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason] end after_transition do |commit_status, transition| diff --git a/app/models/concerns/blob_language_from_git_attributes.rb b/app/models/concerns/blob_language_from_git_attributes.rb new file mode 100644 index 00000000000..70213d22147 --- /dev/null +++ b/app/models/concerns/blob_language_from_git_attributes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Applicable for blob classes with project attribute +module BlobLanguageFromGitAttributes + extend ActiveSupport::Concern + + def language_from_gitattributes + return nil unless project + + repository = project.repository + repository.gitattribute(path, 'gitlab-language') + end +end 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/noteable.rb b/app/models/concerns/noteable.rb index 098eed137ba..eb315058c3a 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -23,7 +23,7 @@ module Noteable end def supports_discussions? - DiscussionNote::NOTEABLE_TYPES.include?(base_class_name) + DiscussionNote.noteable_types.include?(base_class_name) end def discussions_rendered_on_frontend? 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/diff_note.rb b/app/models/diff_note.rb index 95694377fe3..5f59e4832db 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -8,12 +8,14 @@ class DiffNote < Note include DiffPositionableNote include Gitlab::Utils::StrongMemoize - NOTEABLE_TYPES = %w(MergeRequest Commit).freeze + def self.noteable_types + %w(MergeRequest Commit) + end validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } validate :positions_complete validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb index 89d86aaed66..142cbdcdfa6 100644 --- a/app/models/discussion_note.rb +++ b/app/models/discussion_note.rb @@ -5,9 +5,11 @@ # A note of this type can be resolvable. class DiscussionNote < Note # Names of all implementers of `Noteable` that support discussions. - NOTEABLE_TYPES = %w(MergeRequest Issue Commit Snippet).freeze + def self.noteable_types + %w(MergeRequest Issue Commit Snippet) + end - validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } + validates :noteable_type, inclusion: { in: noteable_types } def discussion_class(*) Discussion 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/lfs_object.rb b/app/models/lfs_object.rb index 97bf5d611c2..69c563545bb 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -7,7 +7,7 @@ class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :lfs_objects_projects - scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) } + scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) } validates :oid, presence: true, uniqueness: true @@ -26,7 +26,7 @@ class LfsObject < ActiveRecord::Base end def local_store? - [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store) + file_store == LfsObjectUploader::Store::LOCAL end # rubocop: disable DestroyAll 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/ssh_host_key.rb b/app/models/ssh_host_key.rb new file mode 100644 index 00000000000..b6844dbe870 --- /dev/null +++ b/app/models/ssh_host_key.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Detected SSH host keys are transiently stored in Redis +class SshHostKey + class Fingerprint < Gitlab::SSHPublicKey + attr_reader :index + + def initialize(key, index: nil) + super(key) + + @index = index + end + + def as_json(*) + { bits: bits, fingerprint: fingerprint, type: type, index: index } + end + end + + include ReactiveCaching + + self.reactive_cache_key = ->(key) { [key.class.to_s, key.id] } + + # Do not refresh the data in the background - it is not expected to change. + # This is achieved by making the lifetime shorter than the refresh interval. + self.reactive_cache_refresh_interval = 15.minutes + self.reactive_cache_lifetime = 10.minutes + + def self.find_by(opts = {}) + return nil unless opts.key?(:id) + + project_id, url = opts[:id].split(':', 2) + project = Project.find_by(id: project_id) + + project.presence && new(project: project, url: url) + end + + def self.fingerprint_host_keys(data) + return [] unless data.is_a?(String) + + data + .each_line + .each_with_index + .map { |line, index| Fingerprint.new(line, index: index) } + .select(&:valid?) + end + + attr_reader :project, :url, :compare_host_keys + + def initialize(project:, url:, compare_host_keys: nil) + @project = project + @url = normalize_url(url) + @compare_host_keys = compare_host_keys + end + + def id + [project.id, url].join(':') + end + + def as_json(*) + { + host_keys_changed: host_keys_changed?, + fingerprints: fingerprints, + known_hosts: known_hosts + } + end + + def known_hosts + with_reactive_cache { |data| data[:known_hosts] } + end + + def fingerprints + @fingerprints ||= self.class.fingerprint_host_keys(known_hosts) + end + + # Returns true if the known_hosts data differs from the version passed in at + # initialization as `compare_host_keys`. Comments, ordering, etc, is ignored + def host_keys_changed? + cleanup(known_hosts) != cleanup(compare_host_keys) + end + + def error + with_reactive_cache { |data| data[:error] } + end + + def calculate_reactive_cache + known_hosts, errors, status = + Open3.popen3({}, *%W[ssh-keyscan -T 5 -p #{url.port} -f-]) do |stdin, stdout, stderr, wait_thr| + stdin.puts(url.host) + stdin.close + + [ + cleanup(stdout.read), + cleanup(stderr.read), + wait_thr.value + ] + end + + # ssh-keyscan returns an exit code 0 in several error conditions, such as an + # unknown hostname, so check both STDERR and the exit code + if status.success? && !errors.present? + { known_hosts: known_hosts } + else + Rails.logger.debug("Failed to detect SSH host keys for #{id}: #{errors}") + + { error: 'Failed to detect SSH host keys' } + end + end + + private + + # Remove comments and duplicate entries + def cleanup(data) + data + .to_s + .each_line + .reject { |line| line.start_with?('#') || line.chomp.empty? } + .uniq + .sort + .join + end + + def normalize_url(url) + full_url = ::Addressable::URI.parse(url) + raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh' + + Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}") + rescue Addressable::URI::InvalidURIError + raise ArgumentError.new("Invalid URL") + end +end 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/blob_presenter.rb b/app/presenters/blob_presenter.rb new file mode 100644 index 00000000000..6323c1b3389 --- /dev/null +++ b/app/presenters/blob_presenter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class BlobPresenter < Gitlab::View::Presenter::Simple + presents :blob + + def highlight(plain: nil) + blob.load_all_data! if blob.respond_to?(:load_all_data!) + + Gitlab::Highlight.highlight( + blob.path, + blob.data, + language: blob.language_from_gitattributes, + plain: plain + ) + end +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/job_entity.rb b/app/serializers/job_entity.rb index 0b19cb16955..a0a66511b7b 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -9,7 +9,7 @@ class JobEntity < Grape::Entity expose :started?, as: :started expose :build_path do |build| - build.target_url || path_to(:namespace_project_job, build) + build_path(build) end expose :retry_path, if: -> (*) { retryable? } do |build| @@ -17,7 +17,11 @@ class JobEntity < Grape::Entity end expose :cancel_path, if: -> (*) { cancelable? } do |build| - path_to(:cancel_namespace_project_job, build) + path_to( + :cancel_namespace_project_job, + build, + { continue: { to: build_path(build) } } + ) end expose :play_path, if: -> (*) { playable? } do |build| @@ -60,8 +64,12 @@ class JobEntity < Grape::Entity build.detailed_status(request.current_user) end - def path_to(route, build) - send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend + def path_to(route, build, params = {}) + send("#{route}_path", build.project.namespace, build.project, build, params) # rubocop:disable GitlabSecurity/PublicSend + end + + def build_path(build) + build.target_url || path_to(:namespace_project_job, build) end def failed? 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/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb new file mode 100644 index 00000000000..e2adf755511 --- /dev/null +++ b/app/services/boards/visits/create_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class CreateService < Boards::BaseService + def execute(board) + return unless current_user && Gitlab::Database.read_write? + + if parent.is_a?(Group) + BoardGroupRecentVisit.visited!(current_user, board) + else + BoardProjectRecentVisit.visited!(current_user, board) + end + end + end + end +end diff --git a/app/services/boards/visits/latest_service.rb b/app/services/boards/visits/latest_service.rb new file mode 100644 index 00000000000..9e4c77a6317 --- /dev/null +++ b/app/services/boards/visits/latest_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Boards + module Visits + class LatestService < Boards::BaseService + def execute + return nil unless current_user + + if parent.is_a?(Group) + BoardGroupRecentVisit.latest(current_user, parent) + else + BoardProjectRecentVisit.latest(current_user, parent) + end + end + end + end +end 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/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb index 596c0105ea0..7504773a002 100644 --- a/app/services/resource_events/merge_into_notes_service.rb +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -34,7 +34,7 @@ module ResourceEvents def label_events_by_discussion_id return [] unless resource.respond_to?(:resource_label_events) - events = resource.resource_label_events.includes(:label, :user) + events = resource.resource_label_events.includes(:label, user: :status) events = since_fetch_at(events) events.group_by { |event| event.discussion_id } 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/new.html.haml b/app/views/groups/new.html.haml index 684b51b8552..0904e44a658 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -1,12 +1,12 @@ - @hide_breadcrumbs = true - @hide_top_links = true -- page_title 'New Group' -- header_title "Groups", dashboard_groups_path +- page_title _('New Group') +- header_title _("Groups"), dashboard_groups_path +.page-title-holder + %h1.page-title= _('New group') .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = _('New group') %p - group_docs_path = help_page_path('user/group/index') - group_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_docs_path } @@ -15,24 +15,29 @@ - subgroup_docs_path = help_page_path('user/group/subgroups/index') - subgroup_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: subgroup_docs_path } = s_('Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}.').html_safe % { subgroup_docs_link_start: subgroup_docs_link_start, subgroup_docs_link_end: '</a>'.html_safe } + %p + = _('Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group.') .col-lg-9 = form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f| = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true - .form-group.row.group-description-holder - = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' - .col-sm-10 - = render 'shared/choose_group_avatar_button', f: f - - = render 'shared/old_visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false + .row + .form-group.group-description-holder.col-sm-12 + = f.label :avatar, _("Group avatar"), class: 'label-bold' + %div + = render 'shared/choose_group_avatar_button', f: f - = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled + .form-group.col-sm-12 + %label.label-bold + = _('Visibility level') + %p + = _('Who will be able to see this group?') + = link_to _('View the documentation'), help_page_path("public_access/public_access"), target: '_blank' + = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false - .form-group.row - .offset-sm-2.col-sm-10 - = render 'shared/group_tips' + = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled .form-actions = f.submit 'Create group', class: "btn btn-success" 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/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 156c0d05b02..7c378633667 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -46,7 +46,7 @@ Layout width = f.select :layout, layout_choices, {}, class: 'form-control' .form-text.text-muted - Choose between fixed (max. 1200px) and fluid (100%) application layout. + Choose between fixed (max. 1280px) and fluid (100%) application layout. .form-group = f.label :dashboard, class: 'label-bold' do Default dashboard @@ -56,6 +56,6 @@ Project overview content = f.select :project_view, project_view_choices, {}, class: 'form-control' .form-text.text-muted - Choose what content you want to see on a project’s overview page + Choose what content you want to see on a project’s overview page. .form-group = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/projects/blob/viewers/_highlight_embed.html.haml b/app/views/projects/blob/viewers/_highlight_embed.html.haml index 9bd4ef6ad0b..6a631fef1a9 100644 --- a/app/views/projects/blob/viewers/_highlight_embed.html.haml +++ b/app/views/projects/blob/viewers/_highlight_embed.html.haml @@ -4,4 +4,6 @@ - blob.data.each_line.each_with_index do |_, index| %span.diff-line-num= index + 1 .blob-content{ data: { blob_id: blob.id } } - = highlight(blob.path, blob.data, repository: nil, plain: blob.no_highlighting?) + %pre.code.highlight + %code + = blob.present.highlight diff --git a/app/views/projects/blob/viewers/_text.html.haml b/app/views/projects/blob/viewers/_text.html.haml index a91df321ca0..26ad23da436 100644 --- a/app/views/projects/blob/viewers/_text.html.haml +++ b/app/views/projects/blob/viewers/_text.html.haml @@ -1 +1 @@ -= render 'shared/file_highlight', blob: viewer.blob, repository: @repository += render 'shared/file_highlight', blob: viewer.blob diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index efc2d88172e..5d1bbb077af 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -21,6 +21,7 @@ window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')} window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; + window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests', anchor: 'troubleshooting')}'; #js-vue-mr-widget.mr-widget 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/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml index d14360913a4..82c8ec088e5 100644 --- a/app/views/projects/settings/ci_cd/_badge.html.haml +++ b/app/views/projects/settings/ci_cd/_badge.html.haml @@ -15,14 +15,14 @@ .col-md-2.text-center Markdown .col-md-10.code.js-syntax-highlight - = highlight('.md', badge.to_markdown) + = highlight('.md', badge.to_markdown, language: 'markdown') .row %hr .row .col-md-2.text-center HTML .col-md-10.code.js-syntax-highlight - = highlight('.html', badge.to_html) + = highlight('.html', badge.to_html, language: 'html') .row %hr .row diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 406dccb74fb..e37fd7624be 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link' + = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), title: commit.redacted_full_title_html, class: 'tree-commit-link' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 8b95bdf9747..b4ecd7bbae9 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -39,7 +39,7 @@ .blob-content - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.blob.no_highlighting?) + = highlight(snippet.file_name, chunk[:data]) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 8d64cb5d698..5073e6ad48f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,5 +1,3 @@ -- repository = nil unless local_assigns.key?(:repository) - .file-content.code.js-syntax-highlight .line-numbers - if blob.data.present? @@ -13,4 +11,6 @@ = link_icon = i .blob-content{ data: { blob_id: blob.id } } - = highlight(blob.path, blob.data, repository: repository, plain: blob.no_highlighting?) + %pre.code.highlight + %code + = blob.present.highlight diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index dbed4b94d61..973c756f496 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -2,10 +2,19 @@ - group_path = root_url - group_path << parent.full_path + '/' if parent -.form-group.row - = f.label :path, class: 'col-form-label col-sm-2' do - Group path - .col-sm-10 +.row + .form-group.group-name-holder.col-sm-12 + = f.label :name, class: 'label-bold' do + = _("Group name") + = f.text_field :name, placeholder: 'My Awesome Group', class: 'form-control input-lg', + required: true, + title: _('Please fill in a descriptive name for your group.'), + autofocus: true + +.row + .form-group.col-xs-12.col-sm-8 + = f.label :path, class: 'label-bold' do + = _("Group URL") .input-group.gl-field-error-anchor .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } .input-group-text @@ -13,10 +22,10 @@ - if parent %strong= parent.full_path + '/' = f.hidden_field :parent_id - = f.text_field :path, placeholder: 'open-source', class: 'form-control', + = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, - title: 'Please choose a group path with no special characters.', + title: _('Please choose a group URL with no special characters.'), "data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}" - if @group.persisted? @@ -25,23 +34,17 @@ = succeed '.' do = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank' -.form-group.row.group-name-holder - = f.label :name, class: 'col-form-label col-sm-2' do - Group name - .col-sm-10 - = f.text_field :name, class: 'form-control', - required: true, - title: 'You can choose a descriptive name different from the path.' - - if @group.persisted? - .form-group.row.group-name-holder - = f.label :id, class: 'col-form-label col-sm-2' do - = _("Group ID") - .col-sm-10 + .row + .form-group.group-name-holder.col-sm-8 + = f.label :id, class: 'label-bold' do + = _("Group ID") = f.text_field :id, class: 'form-control', readonly: true -.form-group.row.group-description-holder - = f.label :description, class: 'col-form-label col-sm-2' - .col-sm-10 +.row + .form-group.group-description-holder.col-sm-8 + = f.label :description, class: 'label-bold' do + = _("Group description") + %span (optional) = f.text_area :description, maxlength: 250, class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index e26f5260e5b..c6c5cadc3f5 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -39,14 +39,13 @@ {{ list.issuesSize }} = render_if_exists "shared/boards/components/list_weight" - - if can?(current_user, :admin_list, current_board_parent) - %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", - "@click" => "showNewIssueForm", - "v-if" => 'list.type !== "closed"', - "aria-label" => _("New issue"), - "title" => _("New issue"), - data: { placement: "top", container: "body" } } - = icon("plus", class: "js-no-trigger-collapse") + %button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button", + "@click" => "showNewIssueForm", + "v-if" => "isNewIssueShown", + "aria-label" => _("New issue"), + "title" => _("New issue"), + data: { placement: "top", container: "body" } } + = icon("plus", class: "js-no-trigger-collapse") %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":list" => "list", 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/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 09ddf732ada..f6c7ca70ebd 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -9,11 +9,11 @@ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) - %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) = icon("caret-down") diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index ddc089b0bd7..52c7bc47ca7 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -36,7 +36,7 @@ %li .code.js-syntax-highlight.sherlock-code :preserve - #{highlight("#{@query.id}.sql", @query.formatted_query)} + #{highlight("#{@query.id}.sql", @query.formatted_query, language: 'sql')} .card .card-header diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml index c1ec4b91bb6..5e224f3aa0e 100644 --- a/app/views/sherlock/transactions/_queries.html.haml +++ b/app/views/sherlock/transactions/_queries.html.haml @@ -17,7 +17,7 @@ = t('sherlock.milliseconds') %td .code.js-syntax-highlight.sherlock-code - = highlight("#{query.id}.sql", query.formatted_query) + = highlight("#{query.id}.sql", query.formatted_query, language: 'sql') %td = link_to(t('sherlock.view'), sherlock_transaction_query_path(@transaction, query), 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) @@ -3,6 +3,11 @@ cd $(dirname $0)/.. app_root=$(pwd) +# Switch to experimental PUMA configuration +if [ -n "${EXPERIMENTAL_PUMA}" ]; then + exec bin/web_puma "$@" +fi + unicorn_pidfile="$app_root/tmp/pids/unicorn.pid" unicorn_config="$app_root/config/unicorn.rb" unicorn_cmd="bundle exec unicorn_rails -c $unicorn_config -E $RAILS_ENV" diff --git a/bin/web_puma b/bin/web_puma new file mode 100755 index 00000000000..178fe84800d --- /dev/null +++ b/bin/web_puma @@ -0,0 +1,63 @@ +#!/bin/sh + +set -e + +cd $(dirname $0)/.. +app_root=$(pwd) + +puma_pidfile="$app_root/tmp/pids/puma.pid" +puma_config="$app_root/config/puma.rb" + +spawn_puma() +{ + exec bundle exec puma --config "${puma_config}" "$@" +} + +get_puma_pid() +{ + pid=$(cat "${puma_pidfile}") + if [ -z "$pid" ] ; then + echo "Could not find a PID in $puma_pidfile" + exit 1 + fi + echo "${pid}" +} + +start() +{ + spawn_puma -d +} + +start_foreground() +{ + spawn_puma +} + +stop() +{ + get_puma_pid + kill -QUIT "$(get_puma_pid)" +} + +reload() +{ + kill -USR2 "$(get_puma_pid)" +} + +case "$1" in + start) + start + ;; + start_foreground) + start_foreground + ;; + stop) + stop + ;; + reload) + reload + ;; + *) + echo "Usage: RAILS_ENV=your_env $0 {start|stop|reload}" + ;; +esac diff --git a/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml new file mode 100644 index 00000000000..f5ed6ccf6df --- /dev/null +++ b/changelogs/unreleased/27231-add-license-data-to-projects-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Add license data to projects endpoint +merge_request: 21606 +author: J.D. Bean (@jdbean) +type: added 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/40372-prometheus-dashboard-broken-on-firefox.yml b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml new file mode 100644 index 00000000000..8376fac7abf --- /dev/null +++ b/changelogs/unreleased/40372-prometheus-dashboard-broken-on-firefox.yml @@ -0,0 +1,5 @@ +--- +title: Fix prometheus graphs in firefox +merge_request: 22400 +author: +type: fixed diff --git a/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml new file mode 100644 index 00000000000..103419c1185 --- /dev/null +++ b/changelogs/unreleased/41545-gitlab-merge-request-status-could-not-connect-to-the-ci-server-please-check-your-settings-and-try-again.yml @@ -0,0 +1,5 @@ +--- +title: Reword error message for internal CI unknown pipeline status +merge_request: 22474 +author: +type: changed 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/50962-create-new-group-rename-form-fields-and-update-ui.yml b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml new file mode 100644 index 00000000000..db374e10c36 --- /dev/null +++ b/changelogs/unreleased/50962-create-new-group-rename-form-fields-and-update-ui.yml @@ -0,0 +1,5 @@ +--- +title: 'Create new group: Rename form fields and update UI' +merge_request: +author: +type: other diff --git a/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml new file mode 100644 index 00000000000..13e3bb66430 --- /dev/null +++ b/changelogs/unreleased/51306-fix-inaccessible-dropdown-for-codeless-projects.yml @@ -0,0 +1,5 @@ +--- +title: Fix inaccessible dropdown for code-less projects +merge_request: 22137 +author: +type: other 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/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml new file mode 100644 index 00000000000..8521335c2ea --- /dev/null +++ b/changelogs/unreleased/52115-Link-button-in-markdown-editor-should-recognize-URLs.yml @@ -0,0 +1,5 @@ +--- +title: Link button in markdown editor recognize URLs +merge_request: 1983 +author: Johann Hubert Sonntagbauer +type: changed 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/52384-api-filter-assignee-none-any.yml b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml new file mode 100644 index 00000000000..9acec04d946 --- /dev/null +++ b/changelogs/unreleased/52384-api-filter-assignee-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None/Any option for assignee_id in Issues and Merge Requests API +merge_request: 22598 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml new file mode 100644 index 00000000000..5701e44eb32 --- /dev/null +++ b/changelogs/unreleased/52545-guest-create-issue-in-group-board.yml @@ -0,0 +1,5 @@ +--- +title: Always show new issue button in boards' Open list +merge_request: 22557 +author: Heinrich Lee Yu +type: fixed 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/52993-ldap-rename_provider-rake-task-broken.yml b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml new file mode 100644 index 00000000000..ca78f9a392e --- /dev/null +++ b/changelogs/unreleased/52993-ldap-rename_provider-rake-task-broken.yml @@ -0,0 +1,5 @@ +--- +title: Use gitlab_environment for ldap rake task +merge_request: 22582 +author: +type: fixed diff --git a/changelogs/unreleased/53052-mg-fix-broken-ie11.yml b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml new file mode 100644 index 00000000000..c616efffa6b --- /dev/null +++ b/changelogs/unreleased/53052-mg-fix-broken-ie11.yml @@ -0,0 +1,5 @@ +--- +title: Fix incompatibility with IE11 due to non-transpiled gitlab-ui components +merge_request: 22695 +author: +type: fixed diff --git a/changelogs/unreleased/53133-jobs-list.yml b/changelogs/unreleased/53133-jobs-list.yml new file mode 100644 index 00000000000..2e13edc0e76 --- /dev/null +++ b/changelogs/unreleased/53133-jobs-list.yml @@ -0,0 +1,5 @@ +--- +title: Fix stage dropdown not rendering in different languages +merge_request: 22604 +author: +type: other 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/53227-empty-list.yml b/changelogs/unreleased/53227-empty-list.yml new file mode 100644 index 00000000000..8b222145299 --- /dev/null +++ b/changelogs/unreleased/53227-empty-list.yml @@ -0,0 +1,6 @@ +--- +title: Only renders dropdown for review app changes when we have a list of files to + show. Otherwise will render the regular review app button +merge_request: +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/an-multithreading.yml b/changelogs/unreleased/an-multithreading.yml new file mode 100644 index 00000000000..fca847e6ea4 --- /dev/null +++ b/changelogs/unreleased/an-multithreading.yml @@ -0,0 +1,5 @@ +--- +title: Experimental support for running Puma multithreaded web-server +merge_request: 22372 +author: +type: performance diff --git a/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml new file mode 100644 index 00000000000..30b9ae032d4 --- /dev/null +++ b/changelogs/unreleased/avoid-lock-when-introduce-new-failure-reason.yml @@ -0,0 +1,5 @@ +--- +title: Support backward compatibility when introduce new failure reason +merge_request: 22566 +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/bvl-preload-user-status-for-events.yml b/changelogs/unreleased/bvl-preload-user-status-for-events.yml new file mode 100644 index 00000000000..e13b19b19c1 --- /dev/null +++ b/changelogs/unreleased/bvl-preload-user-status-for-events.yml @@ -0,0 +1,5 @@ +--- +title: Show user status for label events in system notes +merge_request: 22609 +author: +type: fixed diff --git a/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml new file mode 100644 index 00000000000..836b9aa21c5 --- /dev/null +++ b/changelogs/unreleased/bw-automatically-navigate-to-last-board-visited.yml @@ -0,0 +1,5 @@ +--- +title: Automatically navigate to last board visited +merge_request: 22430 +author: +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/fl-missing-i18n.yml b/changelogs/unreleased/fl-missing-i18n.yml new file mode 100644 index 00000000000..d41a691e636 --- /dev/null +++ b/changelogs/unreleased/fl-missing-i18n.yml @@ -0,0 +1,5 @@ +--- +title: Adds missing i18n to pipelines table +merge_request: +author: +type: other diff --git a/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml new file mode 100644 index 00000000000..a881c304f34 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-lib-gitlab-ci.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string for lib/gitlab/ci +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/gl-ui-modal.yml b/changelogs/unreleased/gl-ui-modal.yml new file mode 100644 index 00000000000..fbdb8260d24 --- /dev/null +++ b/changelogs/unreleased/gl-ui-modal.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's modal from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gl-ui-pagination.yml b/changelogs/unreleased/gl-ui-pagination.yml new file mode 100644 index 00000000000..cf73d6a1f8f --- /dev/null +++ b/changelogs/unreleased/gl-ui-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Remove gitlab-ui's pagination from global +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gt-fix-ide-typos-in-props.yml b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml new file mode 100644 index 00000000000..a81b227c82f --- /dev/null +++ b/changelogs/unreleased/gt-fix-ide-typos-in-props.yml @@ -0,0 +1,5 @@ +--- +title: Fix IDE typos in props +merge_request: 22685 +author: George Tsiolis +type: other 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/jramsay-42673-commit-tooltip.yml b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml new file mode 100644 index 00000000000..083cd1a54a0 --- /dev/null +++ b/changelogs/unreleased/jramsay-42673-commit-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Add commit message to commit tree anchor title +merge_request: 22585 +author: +type: fixed 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-search-relative-urls.yml b/changelogs/unreleased/sh-fix-search-relative-urls.yml new file mode 100644 index 00000000000..2545e9ca553 --- /dev/null +++ b/changelogs/unreleased/sh-fix-search-relative-urls.yml @@ -0,0 +1,5 @@ +--- +title: Fix search "all in GitLab" not working with relative URLs +merge_request: 22644 +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/changelogs/unreleased/tc-index-lfs-objects-file-store.yml b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml new file mode 100644 index 00000000000..90e80cb1ef1 --- /dev/null +++ b/changelogs/unreleased/tc-index-lfs-objects-file-store.yml @@ -0,0 +1,5 @@ +--- +title: Enhance performance of counting local LFS objects +merge_request: 22143 +author: +type: performance diff --git a/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml new file mode 100644 index 00000000000..4ea1d3f8256 --- /dev/null +++ b/changelogs/unreleased/winh-pipeline-actions-dynamic-timer.yml @@ -0,0 +1,5 @@ +--- +title: Add dynamic timer for delayed jobs in pipelines list +merge_request: 22621 +author: +type: changed diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 146c4b1e024..8052880cc3d 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -26,9 +26,25 @@ Sidekiq.configure_server do |config| end if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? - unless Sidekiq.server? - Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start + Gitlab::Cluster::LifecycleEvents.on_worker_start do + defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change + + unless Sidekiq.server? + Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start + end + + Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start end +end - Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start +Gitlab::Cluster::LifecycleEvents.on_master_restart do + # The following is necessary to ensure stale Prometheus metrics don't + # accumulate over time. It needs to be done in this hook as opposed to + # inside an init script to ensure metrics files aren't deleted after new + # unicorn workers start after a SIGUSR2 is received. + prometheus_multiproc_dir = ENV['prometheus_multiproc_dir'] + if prometheus_multiproc_dir + old_metrics = Dir[File.join(prometheus_multiproc_dir, '*.db')] + FileUtils.rm_rf(old_metrics) + end end diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index eccf82ab8dc..c8d261d415e 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -158,7 +158,9 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? GC::Profiler.enable - Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start + Gitlab::Cluster::LifecycleEvents.on_worker_start do + Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start + end module TrackNewRedisConnections def connect(*args) diff --git a/config/initializers/active_record_lifecycle.rb b/config/initializers/active_record_lifecycle.rb new file mode 100644 index 00000000000..7fa37121efc --- /dev/null +++ b/config/initializers/active_record_lifecycle.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Don't handle sidekiq configuration as it +# has its own special active record configuration here +if defined?(ActiveRecord::Base) && !Sidekiq.server? + Gitlab::Cluster::LifecycleEvents.on_worker_start do + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.establish_connection + + Rails.logger.debug("ActiveRecord connection established") + end + end +end + +if defined?(ActiveRecord::Base) + Gitlab::Cluster::LifecycleEvents.on_before_fork do + # the following is highly recommended for Rails + "preload_app true" + # as there's no need for the master process to hold a connection + ActiveRecord::Base.connection.disconnect! + + Rails.logger.debug("ActiveRecord connection disconnected") + end +end 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/initializers/macos.rb b/config/initializers/macos.rb new file mode 100644 index 00000000000..f410af6ed47 --- /dev/null +++ b/config/initializers/macos.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +if /darwin/ =~ RUBY_PLATFORM + Gitlab::Cluster::LifecycleEvents.on_before_fork do + require 'fiddle' + + # Dynamically load Foundation.framework, ~implicitly~ initialising + # the Objective-C runtime before any forking happens in Unicorn + # + # From https://bugs.ruby-lang.org/issues/14009 + Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation' + end +end diff --git a/config/initializers/rbtrace.rb b/config/initializers/rbtrace.rb new file mode 100644 index 00000000000..6a1b71bf4bd --- /dev/null +++ b/config/initializers/rbtrace.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +if ENV['ENABLE_RBTRACE'] + Gitlab::Cluster::LifecycleEvents.on_worker_start do + # Unicorn clears out signals before it forks, so rbtrace won't work + # unless it is enabled after the fork. + require 'rbtrace' + end +end diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb index 25003cf0239..f0f74954eef 100644 --- a/config/initializers/routing_draw.rb +++ b/config/initializers/routing_draw.rb @@ -1,7 +1,3 @@ # Adds draw method into Rails routing -# It allows us to keep routing splitted into files -class ActionDispatch::Routing::Mapper - def draw(routes_name) - instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb"))) - end -end +# It allows us to keep routing split into files +ActionDispatch::Routing::Mapper.prepend Gitlab::Patch::DrawRoute diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index bc6b7aed6aa..565efc858d1 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -14,8 +14,6 @@ Sidekiq.default_worker_options = { retry: 3 } enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' Sidekiq.configure_server do |config| - require 'rbtrace' if ENV['ENABLE_RBTRACE'] - config.redis = queues_config_hash config.server_middleware do |chain| diff --git a/config/locales/en.yml b/config/locales/en.yml index 795e5d4e6bc..0a43a1d9a6b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -8,6 +8,8 @@ en: issue_link: source: Source issue target: Target issue + group: + path: Group URL errors: messages: label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one." diff --git a/config/puma.example.development.rb b/config/puma.example.development.rb new file mode 100644 index 00000000000..490c940077a --- /dev/null +++ b/config/puma.example.development.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# ----------------------------------------------------------------------- +# This file is used by the GDK to generate a default config/puma.rb file +# Note that `/home/git` will be substituted for the actual GDK root +# directory when this file is generated +# ----------------------------------------------------------------------- + +# Load "path" as a rackup file. +# +# The default is "config.ru". +# +rackup 'config.ru' +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +state_path '/home/git/gitlab/tmp/pids/puma.state' + +stdout_redirect '/home/git/gitlab/log/puma.stdout.log', + '/home/git/gitlab/log/puma.stderr.log', + true + +# Configure "min" to be the minimum number of threads to use to answer +# requests and "max" the maximum. +# +# The default is "0, 16". +# +threads 1, 4 + +# By default, workers accept all requests and queue them to pass to handlers. +# When false, workers accept the number of simultaneous requests configured. +# +# Queueing requests generally improves performance, but can cause deadlocks if +# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612 +# +# When set to false this may require a reverse proxy to handle slow clients and +# queue requests before they reach puma. This is due to disabling HTTP keepalive +queue_requests false + +# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only +# accepted protocols. +bind 'unix:///home/git/gitlab.socket' + +workers 2 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +on_restart do + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do + # Signal to the puma killer + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER'] + + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end + +# Preload the application before starting the workers; this conflicts with +# phased restart feature. (off by default) + +preload_app! + +tag 'gitlab-puma-worker' + +# Verifies that all workers have checked in to the master process within +# the given timeout. If not the worker process will be restarted. Default +# value is 60 seconds. +# +worker_timeout 60 diff --git a/config/routes.rb b/config/routes.rb index c081ca9672a..37c7f98ec98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,6 +34,8 @@ Rails.application.routes.draw do match '*all', via: [:get, :post], to: proc { [404, {}, ['']] } end + draw :oauth + use_doorkeeper_openid_connect # Autocomplete @@ -78,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/admin.rb b/config/routes/admin.rb index fb29c4748c1..af333bdc748 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -71,6 +71,7 @@ namespace :admin do resource :logs, only: [:show] resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] + resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } @@ -104,6 +105,7 @@ namespace :admin do resource :application_settings, only: [:show, :update] do resources :services, only: [:index, :edit, :update] + get :usage_data put :reset_registration_token put :reset_health_check_token diff --git a/config/routes/group.rb b/config/routes/group.rb index 602bbe837cf..2328b50b760 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + resources :groups, only: [:index, :new, :create] do post :preview_markdown end @@ -63,7 +65,6 @@ constraints(::Constraints::GroupUrlConstrainer.new) do end end - # On CE only index and show actions are needed resources :boards, only: [:index, :show] resources :runners, only: [:index, :edit, :update, :destroy, :show] do diff --git a/config/routes/project.rb b/config/routes/project.rb index 9cbd5b644f6..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 @@ -178,6 +177,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resource :mirror, only: [:show, :update] do member do + get :ssh_host_keys, constraints: { format: :json } post :update_now end end diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index e06cce3e97a..4637eb8bc6e 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -81,22 +81,16 @@ preload_app true # fast LAN. check_client_connection false +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + before_exec do |server| - # The following is necessary to ensure stale Prometheus metrics don't - # accumulate over time. It needs to be done in this hook as opposed to - # inside an init script to ensure metrics files aren't deleted after new - # unicorn workers start after a SIGUSR2 is received. - if ENV['prometheus_multiproc_dir'] - old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')] - FileUtils.rm_rf(old_metrics) - end + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart end before_fork do |server, worker| - # the following is highly recommended for Rails + "preload_app true" - # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) && - ActiveRecord::Base.connection.disconnect! + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork # The following is only recommended for memory/DB-constrained # installations. It is not needed if your system can house @@ -124,25 +118,10 @@ before_fork do |server, worker| end after_fork do |server, worker| - # Unicorn clears out signals before it forks, so rbtrace won't work - # unless it is enabled after the fork. - require 'rbtrace' if ENV['ENABLE_RBTRACE'] + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start # per-process listener ports for debugging/admin/migrations # addr = "127.0.0.1:#{9293 + worker.nr}" # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) - - # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) && - ActiveRecord::Base.establish_connection - - # reset prometheus client, this will cause any opened metrics files to be closed - defined?(::Prometheus::Client.reinitialize_on_pid_change) && - Prometheus::Client.reinitialize_on_pid_change - - # if preload_app is true, then you may also want to check and - # restart any other shared sockets/descriptors such as Memcached, - # and Redis. TokyoCabinet file handles are safe to reuse - # between any number of forked children (assuming your kernel - # correctly implements pread()/pwrite() system calls) end diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development index f31df66015a..f7541bb9d55 100644 --- a/config/unicorn.rb.example.development +++ b/config/unicorn.rb.example.development @@ -1,32 +1,61 @@ +# frozen_string_literal: true + +# ------------------------------------------------------------------------- +# This file is used by the GDK to generate a default config/unicorn.rb file +# Note that `/home/git` will be substituted for the actual GDK root +# directory when this file is generated +# ------------------------------------------------------------------------- + worker_processes 2 timeout 60 +listen '/home/git/gitlab.socket' + preload_app true check_client_connection false +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + before_fork do |server, worker| - # the following is highly recommended for Rails + "preload_app true" - # as there's no need for the master process to hold a connection - defined?(ActiveRecord::Base) && - ActiveRecord::Base.connection.disconnect! - - if /darwin/ =~ RUBY_PLATFORM - require 'fiddle' - - # Dynamically load Foundation.framework, ~implicitly~ initialising - # the Objective-C runtime before any forking happens in Unicorn - # - # From https://bugs.ruby-lang.org/issues/14009 - Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation' + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork + + # The following is only recommended for memory/DB-constrained + # installations. It is not needed if your system can house + # twice as many worker_processes as you have configured. + # + # This allows a new master process to incrementally + # phase out the old master process with SIGTTOU to avoid a + # thundering herd (especially in the "preload_app false" case) + # when doing a transparent upgrade. The last worker spawned + # will then kill off the old master process with a SIGQUIT. + old_pid = "#{server.config[:pid]}.oldbin" + if old_pid != server.pid + begin + sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU + Process.kill(sig, File.read(old_pid).to_i) + rescue Errno::ENOENT, Errno::ESRCH + end end + # + # Throttle the master from forking too quickly by sleeping. Due + # to the implementation of standard Unix signal handlers, this + # helps (but does not completely) prevent identical, repeated signals + # from being lost when the receiving process is busy. + # sleep 1 end after_fork do |server, worker| - # Unicorn clears out signals before it forks, so rbtrace won't work - # unless it is enabled after the fork. - require 'rbtrace' if ENV['ENABLE_RBTRACE'] + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start - # the following is *required* for Rails + "preload_app true", - defined?(ActiveRecord::Base) && - ActiveRecord::Base.establish_connection + # per-process listener ports for debugging/admin/migrations + # addr = "127.0.0.1:#{9293 + worker.nr}" + # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) 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/20181005110927_add_index_to_lfs_objects_file_store.rb b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb new file mode 100644 index 00000000000..d09543aa4cc --- /dev/null +++ b/db/migrate/20181005110927_add_index_to_lfs_objects_file_store.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToLfsObjectsFileStore < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :lfs_objects, :file_store + end + + def down + remove_concurrent_index :lfs_objects, :file_store + end +end diff --git a/db/migrate/20181010235606_create_board_project_recent_visits.rb b/db/migrate/20181010235606_create_board_project_recent_visits.rb new file mode 100644 index 00000000000..426f41e202a --- /dev/null +++ b/db/migrate/20181010235606_create_board_project_recent_visits.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateBoardProjectRecentVisits < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :board_project_recent_visits, id: :bigserial do |t| + t.timestamps_with_timezone null: false + + t.references :user, index: true, foreign_key: { on_delete: :cascade } + t.references :project, index: true, foreign_key: { on_delete: :cascade } + t.references :board, index: true, foreign_key: { on_delete: :cascade } + end + + add_index :board_project_recent_visits, [:user_id, :project_id, :board_id], unique: true, name: 'index_board_project_recent_visits_on_user_project_and_board' + 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/20181016152238_create_board_group_recent_visits.rb b/db/migrate/20181016152238_create_board_group_recent_visits.rb new file mode 100644 index 00000000000..1e55dc8658e --- /dev/null +++ b/db/migrate/20181016152238_create_board_group_recent_visits.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateBoardGroupRecentVisits < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :board_group_recent_visits, id: :bigserial do |t| + t.timestamps_with_timezone null: false + + t.references :user, index: true, foreign_key: { on_delete: :cascade } + t.references :board, index: true, foreign_key: { on_delete: :cascade } + t.references :group, references: :namespace, column: :group_id, index: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + end + + add_index :board_group_recent_visits, [:user_id, :group_id, :board_id], unique: true, name: 'index_board_group_recent_visits_on_user_group_and_board' + 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 ddfccbba678..6b0bb33f969 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: 20181013005024) do +ActiveRecord::Schema.define(version: 20181017001059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -204,6 +204,32 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree + create_table "board_group_recent_visits", id: :bigserial, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "user_id" + t.integer "board_id" + t.integer "group_id" + end + + add_index "board_group_recent_visits", ["board_id"], name: "index_board_group_recent_visits_on_board_id", using: :btree + add_index "board_group_recent_visits", ["group_id"], name: "index_board_group_recent_visits_on_group_id", using: :btree + add_index "board_group_recent_visits", ["user_id", "group_id", "board_id"], name: "index_board_group_recent_visits_on_user_group_and_board", unique: true, using: :btree + add_index "board_group_recent_visits", ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree + + create_table "board_project_recent_visits", id: :bigserial, force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "user_id" + t.integer "project_id" + t.integer "board_id" + end + + add_index "board_project_recent_visits", ["board_id"], name: "index_board_project_recent_visits_on_board_id", using: :btree + add_index "board_project_recent_visits", ["project_id"], name: "index_board_project_recent_visits_on_project_id", using: :btree + add_index "board_project_recent_visits", ["user_id", "project_id", "board_id"], name: "index_board_project_recent_visits_on_user_project_and_board", unique: true, using: :btree + add_index "board_project_recent_visits", ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree + create_table "boards", force: :cascade do |t| t.integer "project_id" t.datetime "created_at", null: false @@ -573,6 +599,14 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 @@ -628,6 +662,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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 @@ -1132,6 +1167,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do t.integer "file_store" end + add_index "lfs_objects", ["file_store"], name: "index_lfs_objects_on_file_store", using: :btree add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree create_table "lfs_objects_projects", force: :cascade do |t| @@ -1513,7 +1549,7 @@ ActiveRecord::Schema.define(version: 20181013005024) 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" @@ -1521,9 +1557,11 @@ ActiveRecord::Schema.define(version: 20181013005024) 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| @@ -2306,6 +2344,12 @@ ActiveRecord::Schema.define(version: 20181013005024) do add_foreign_key "application_settings", "users", column: "usage_stats_set_by_user_id", name: "fk_964370041d", on_delete: :nullify add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "badges", "projects", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "boards", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "board_group_recent_visits", "users", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "boards", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "projects", on_delete: :cascade + add_foreign_key "board_project_recent_visits", "users", on_delete: :cascade add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade add_foreign_key "chat_teams", "namespaces", on_delete: :cascade @@ -2340,6 +2384,8 @@ ActiveRecord::Schema.define(version: 20181013005024) 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/issues.md b/doc/api/issues.md index cc1d6834a20..57e861bc62e 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -40,7 +40,7 @@ GET /issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone. `Any+Milestone` lists all issues that have an assigned milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | @@ -154,7 +154,7 @@ GET /groups/:id/issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -268,7 +268,7 @@ GET /projects/:id/issues?my_reaction_emoji=star | `milestone` | string | no | The milestone title. `No+Milestone` lists all issues with no milestone | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 862ee398a84..0291b7e00c2 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -42,7 +42,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. | | `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -166,7 +166,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | @@ -279,7 +279,7 @@ Parameters: | `updated_before` | datetime | no | Return merge requests updated on or before the given time | | `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | | `target_branch` | string | no | Return merge requests with the given target branch | diff --git a/doc/api/projects.md b/doc/api/projects.md index 947e7db9c52..961241f31e1 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -451,6 +451,7 @@ GET /projects/:id | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `statistics` | boolean | no | Include project statistics | +| `license` | boolean | no | Include project license data | | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) | ```json @@ -508,6 +509,14 @@ GET /projects/:id }, "archived": false, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -572,6 +581,14 @@ If the project is a fork, and you provide a valid token to authenticate, the "http_url_to_repo":"https://gitlab.com/gitlab-org/gitlab-ce.git", "web_url":"https://gitlab.com/gitlab-org/gitlab-ce", "avatar_url":"https://assets.gitlab-static.net/uploads/-/system/project/avatar/13083/logo-extra-whitespace.png", + "license_url": "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE", + "license": { + "key": "mit", + "name": "MIT License", + "nickname": null, + "html_url": "http://choosealicense.com/licenses/mit/", + "source_url": "https://opensource.org/licenses/MIT", + }, "star_count":3812, "forks_count":3561, "last_activity_at":"2018-01-02T11:40:26.570Z", @@ -905,6 +922,14 @@ Example response: "import_status": "none", "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 1, @@ -983,6 +1008,14 @@ Example response: "import_status": "none", "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -1101,6 +1134,14 @@ Example response: }, "archived": true, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, @@ -1197,6 +1238,14 @@ Example response: }, "archived": false, "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png", + "license_url": "http://example.com/diaspora/diaspora-client/blob/master/LICENSE", + "license": { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "nickname": "GNU LGPLv3", + "html_url": "http://choosealicense.com/licenses/lgpl-3.0/", + "source_url": "http://www.gnu.org/licenses/lgpl-3.0.txt" + }, "shared_runners_enabled": true, "forks_count": 0, "star_count": 0, 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/ci/yaml/README.md b/doc/ci/yaml/README.md index 4b2a6ccc7e4..981aa101dd3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -474,6 +474,7 @@ docker build: changes: - Dockerfile - docker/scripts/* + - dockerfiles/**/* ``` In the scenario above, if you are pushing multiple commits to GitLab to an @@ -482,6 +483,7 @@ one of the commits contains changes to either: - The `Dockerfile` file. - Any of the files inside `docker/scripts/` directory. +- Any of the files and subfolders inside `dockerfiles` directory. CAUTION: **Warning:** There are some caveats when using this feature with new branches and tags. See diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index f9e6efa2c30..b6f053ff0e9 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -171,7 +171,7 @@ There are a few gotchas with it: class Base def execute return unless enabled? - + # ... # ... end @@ -185,12 +185,12 @@ There are a few gotchas with it: class Base def execute return unless enabled? - + do_something end - + private - + def do_something # ... # ... @@ -204,14 +204,14 @@ There are a few gotchas with it: ```ruby module EE::Base extend ::Gitlab::Utils::Override - + override :do_something def do_something # Follow the above pattern to call super and extend it end end ``` - + This would require updating CE first, or make sure this is back ported to CE. When prepending, place them in the `ee/` specific sub-directory, and @@ -332,6 +332,21 @@ full implementation details. [ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373 [ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199 +### Code in `config/routes` + +When we add `draw :admin` in `config/routes.rb`, the application will try to +load the file located in `config/routes/admin.rb`, and also try to load the +file located in `ee/config/routes/admin.rb`. + +In EE, it should at least load one file, at most two files. If it cannot find +any files, an error will be raised. In CE, since we don't know if there will +be an EE route, it will not raise any errors even if it cannot find anything. + +This means if we want to extend a particular CE route file, just add the same +file located in `ee/config/routes`. If we want to add an EE only route, we +could still put `draw :ee_only` in both CE and EE, and add +`ee/config/routes/ee_only.rb` in EE, similar to `render_if_exists`. + ### Code in `app/controllers/` In controllers, the most common type of conflict is with `before_action` that diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md index dada59ce242..b65fbc9d958 100644 --- a/doc/development/rolling_out_changes_using_feature_flags.md +++ b/doc/development/rolling_out_changes_using_feature_flags.md @@ -152,14 +152,31 @@ in RC1, followed by the feature flag being removed in RC2. This in turn means the feature will be stable by the time we publish a stable package around the 22nd of the month. -## Undefined feature flags default to "on" +## Implicit feature flags -By default, the [`Project#feature_available?`][project-fa], +The [`Project#feature_available?`][project-fa], [`Namespace#feature_available?`][namespace-fa] (EE), and -[`License.feature_available?`][license-fa] (EE) methods will check if the -specified feature is behind a feature flag. Unless the feature is explicitly -disabled or limited to a percentage of users, the feature flag check will -default to `true`. +[`License.feature_available?`][license-fa] (EE) methods all implicitly check for +a feature flag by the same name as the provided argument. + +For example if a feature is license-gated, there's no need to add an additional +explicit feature flag check since the flag will be checked as part of the +`License.feature_available?` call. Similarly, there's no need to "clean up" a +feature flag once the feature has reached general availability. + +You'd still want to use an explicit `Feature.enabled?` check if your new feature +isn't gated by a License or Plan. + +[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68 +[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85 +[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300 + +### Undefined feature flags default to "on" + +An important side-effect of the [implicit feature +flags][#implicit-feature-flags] mentioned above is that unless the feature is +explicitly disabled or limited to a percentage of users, the feature flag check +will default to `true`. As an example, if you were to ship the backend half of a feature behind a flag, you'd want to explicitly disable that flag until the frontend half is also ready @@ -171,7 +188,3 @@ to be shipped. You can do this via ChatOps: Note that you can do this at any time, even before the merge request using the flag has been merged! - -[project-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/app/models/project_feature.rb#L63-68 -[namespace-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85 -[license-fa]: https://gitlab.com/gitlab-org/gitlab-ee/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300 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/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 2b9cce6539f..76f5495ff78 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -9,37 +9,39 @@ You can only restore a backup to **exactly the same version and type (CE/EE)** of GitLab on which it was created. The best way to migrate your repositories from one server to another is through backup restore. -## Backup +## Requirements -GitLab provides a simple command line interface to backup your whole installation, -and is flexible enough to fit your needs. +In order to be able to backup and restore, you need two essential tools +installed on your system. -### Requirements +### Rsync -* rsync +If you installed GitLab: -If you're using GitLab with the Omnibus package, you're all set. If you -installed GitLab from source, make sure you have rsync installed. +- Using the Omnibus package, you're all set. +- From source, make sure `rsync` is installed: -If you're using Ubuntu, you could run: + ```sh + # Debian/Ubuntu + sudo apt-get install rsync -``` -sudo apt-get install -y rsync -``` + # RHEL/CentOS + sudo yum install rsync + ``` -* tar +### Tar Backup and restore tasks use `tar` under the hood to create and extract archives. Ensure you have version 1.30 or above of `tar` available in your system. To check the version, run: -``` +```sh tar --version ``` -### Backup timestamp +## Backup timestamp ->**Note:** +NOTE: **Note:** In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to `EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25` would become `1493107454_2018_04_25_10.6.4-ce`. @@ -54,30 +56,46 @@ available. For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`, then the timestamp is `1493107454_2018_04_25_10.6.4-ce`. -### Creating a backup of the GitLab system +## Creating a backup of the GitLab system + +GitLab provides a simple command line interface to backup your whole instance. +It backs up your: + +- Database +- Attachments +- Git repositories data +- CI/CD job output logs +- CI/CD job artifacts +- LFS objects +- Container Registry images +- GitLab Pages content + +CAUTION: **Warning:** +GitLab does not back up any configuration files, SSL certificates, or system files. +You are highly advised to [read about storing configuration files](#storing-configuration-files). Use this command if you've installed GitLab with the Omnibus package: -``` +```sh sudo gitlab-rake gitlab:backup:create ``` Use this if you've installed GitLab from source: -``` +```sh sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` If you are running GitLab within a Docker container, you can run the backup from the host: -``` +```sh docker exec -t <container name> gitlab-rake gitlab:backup:create ``` If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can -run the backup task on the gitlab application pod using kubectl +run the backup task on the gitlab application pod using kubectl: -``` +```sh kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create ``` @@ -110,9 +128,50 @@ Deleting tmp directories...[DONE] Deleting old backups... [SKIPPING] ``` +## Storing configuration files + +A backup performed by the [raketask GitLab provides](#creating-a-backup-of-the-gitlab-system) +does **not** store your configuration files. The primary reason for this is that your +database contains encrypted information for two-factor authentication, the CI/CD +'secure variables', etc. Storing encrypted information along with its key in the +same place defeats the purpose of using encryption in the first place. + +CAUTION: **Warning:** +The secrets file is essential to preserve your database encryption key. + +At the very **minimum**, you must backup: + +For Omnibus: + +- `/etc/gitlab/gitlab-secrets.json` +- `/etc/gitlab/gitlab.rb` + +For installation from source: + +- `/home/git/gitlab/config/secrets.yml` +- `/home/git/gitlab/config/gitlab.yml` + +For [Docker installations](https://docs.gitlab.com/omnibus/docker/), you must +back up the volume where the configuration files are stored. If you have created +the GitLab container according to the documentation, it should be under +`/srv/gitlab/config`. + +You may also want to back up any TLS keys and certificates, and your +[SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + +If you use Omnibus GitLab, see some additional information +[to backup your configuration](https://docs.gitlab.com/omnibus/settings/backups.html). + +In the unlikely event that the secrets file is lost, see the +[troubleshooting section](#when-the-secrets-file-is-lost). + +## Backup options + +The command line tool GitLab provides to backup your instance can take more options. + ### Backup strategy option -> **Note:** Introduced as an option in GitLab 8.17. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8728) in GitLab 8.17. The default backup strategy is to essentially stream data from the respective data locations to the backup using the Linux command `tar` and `gzip`. This works @@ -129,8 +188,11 @@ so the problem doesn't compound, but it could be a considerable change for large installations. This is why the `copy` strategy is not the default in 8.17. To use the `copy` strategy instead of the default streaming strategy, specify -`STRATEGY=copy` in the Rake task command. For example, -`sudo gitlab-rake gitlab:backup:create STRATEGY=copy`. +`STRATEGY=copy` in the Rake task command. For example: + +```sh +sudo gitlab-rake gitlab:backup:create STRATEGY=copy +``` ### Excluding specific directories from the backup @@ -151,11 +213,15 @@ Use a comma to specify several options at the same time: All wikis will be backed up as part of the `repositories` group. Non-existent wikis will be skipped during a backup. -``` -# use this command if you've installed GitLab with the Omnibus package +For Omnibus GitLab packages: + +```sh sudo gitlab-rake gitlab:backup:create SKIP=db,uploads +``` + +For installations from source: -# if you've installed GitLab from source +```sh sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production ``` @@ -208,7 +274,7 @@ This example can be used for a bucket in Amsterdam (AMS3). 1. [Reconfigure GitLab] for the changes to take effect -CAUTION: **Warning:** +NOTE: **Note:** If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the usage of backup encryption. Remove or comment the line that contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces @@ -370,33 +436,43 @@ backups will be copied to, and will be created if it does not exist. If the directory that you want to copy the tarballs to is the root of your mounted directory, just use `.` instead. -For omnibus packages: -```ruby -gitlab_rails['backup_upload_connection'] = { - :provider => 'Local', - :local_root => '/mnt/backups' -} +For Omnibus GitLab packages: -# The directory inside the mounted folder to copy backups to -# Use '.' to store them in the root directory -gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups' -``` +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['backup_upload_connection'] = { + :provider => 'Local', + :local_root => '/mnt/backups' + } + + # The directory inside the mounted folder to copy backups to + # Use '.' to store them in the root directory + gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups' + ``` + +1. [Reconfigure GitLab] for the changes to take effect. + +--- For installations from source: -```yaml - backup: - # snip - upload: - # Fog storage connection settings, see http://fog.io/storage/ . - connection: - provider: Local - local_root: '/mnt/backups' - # The directory inside the mounted folder to copy backups to - # Use '.' to store them in the root directory - remote_directory: 'gitlab_backups' -``` +1. Edit `home/git/gitlab/config/gitlab.yml`: + + ```yaml + backup: + upload: + # Fog storage connection settings, see http://fog.io/storage/ . + connection: + provider: Local + local_root: '/mnt/backups' + # The directory inside the mounted folder to copy backups to + # Use '.' to store them in the root directory + remote_directory: 'gitlab_backups' + ``` + +1. [Restart GitLab] for the changes to take effect. ### Backup archive permissions @@ -405,45 +481,56 @@ will have owner/group git:git and 0600 permissions by default. This is meant to avoid other system users reading GitLab's data. If you need the backup archives to have different permissions you can use the 'archive_permissions' setting. -``` -# In /etc/gitlab/gitlab.rb, for omnibus packages -gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable -``` +For Omnibus GitLab packages: -``` -# In gitlab.yml, for installations from source: - backup: - archive_permissions: 0644 # Makes the backup archives world-readable -``` +1. Edit `/etc/gitlab/gitlab.rb`: -### Storing configuration files + ```ruby + gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable + ``` + +1. [Reconfigure GitLab] for the changes to take effect. + +--- + +For installations from source: -Please be informed that a backup does not store your configuration -files. One reason for this is that your database contains encrypted -information for two-factor authentication. Storing encrypted -information along with its key in the same place defeats the purpose -of using encryption in the first place! +1. Edit `/home/git/gitlab/config/gitlab.yml`: -If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). -If you have a cookbook installation there should be a copy of your configuration in Chef. -If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). + ```yaml + backup: + archive_permissions: 0644 # Makes the backup archives world-readable + ``` -At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and -`/etc/gitlab/gitlab-secrets.json` (Omnibus), or -`/home/git/gitlab/config/secrets.yml` (source) to preserve your database -encryption key. +1. [Restart GitLab] for the changes to take effect. ### Configuring cron to make daily backups ->**Note:** +NOTE: **Note:** The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). -**For Omnibus installations** +For Omnibus GitLab packages: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + ## Limit backup lifetime to 7 days - 604800 seconds + gitlab_rails['backup_keep_time'] = 604800 + ``` + +1. [Reconfigure GitLab] for the changes to take effect. + +Note that the `backup_keep_time` configuration option only manages local +files. GitLab does not automatically prune old files stored in a third-party +object storage (e.g., AWS S3) because the user may not have permission to list +and delete files. We recommend that you configure the appropriate retention +policy for your object storage. For example, you can configure [the S3 backup +policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). To schedule a cron job that backs up your repositories and GitLab metadata, use the root user: -``` +```sh sudo su - crontab -e ``` @@ -455,26 +542,24 @@ There, add the following line to schedule the backup for everyday at 2 AM: ``` You may also want to set a limited lifetime for backups to prevent regular -backups using all your disk space. To do this add the following lines to -`/etc/gitlab/gitlab.rb` and reconfigure: +backups using all your disk space. -``` -# limit backup lifetime to 7 days - 604800 seconds -gitlab_rails['backup_keep_time'] = 604800 -``` +--- -Note that the `backup_keep_time` configuration option only manages local -files. GitLab does not automatically prune old files stored in a third-party -object storage (e.g., AWS S3) because the user may not have permission to list -and delete files. We recommend that you configure the appropriate retention -policy for your object storage. For example, you can configure [the S3 backup -policy as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3). +For installations from source: + +1. Edit `home/git/gitlab/config/gitlab.yml`: -**For installation from source** + ```yaml + backup: + ## Limit backup lifetime to 7 days - 604800 seconds + keep_time: 604800 + ``` -``` -cd /home/git/gitlab -sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups +1. [Restart GitLab] for the changes to take effect. + + +```sh sudo -u git crontab -e # Edit the crontab for the git user ``` @@ -711,5 +796,53 @@ Those objects have no influence on the database backup/restore but they give thi For more information see similar questions on postgresql issue tracker[here](http://www.postgresql.org/message-id/201110220712.30886.adrian.klaver@gmail.com) and [here](http://www.postgresql.org/message-id/2039.1177339749@sss.pgh.pa.us) as well as [stack overflow](http://stackoverflow.com/questions/4368789/error-must-be-owner-of-language-plpgsql). +### When the secrets file is lost + +If you have failed to [back up the secrets file](#storing-configuration-files), +then users with 2FA enabled will not be able to log into GitLab. In that case, +you need to [disable 2FA for everyone](../security/two_factor_authentication.md#disabling-2fa-for-everyone). + +In the case of CI/CD, if your project has secure variables set, you might experience +some weird behavior, like stuck jobs or 500 errors. In that case, you can try +deleting the `ci_variables` table from the database. + +CAUTION: **Warning:** +Use the following commands at your own risk, and make sure you've taken a +backup beforehand. + +1. Enter the Rails console: + + For Omnibus GitLab packages: + + ```sh + sudo gitlab-rails dbconsole + ``` + + For installations from source: + + ```sh + sudo -u git -H bundle exec rails dbconsole RAILS_ENV=production + ``` + +1. Check the `ci_variables` table: + + ```sql + SELECT * FROM public."ci_variables"; + ``` + + Those are the variables that you need to delete. + +1. Drop the table: + + ```sql + DELETE FROM ci_variables; + ``` + +1. You may need to reconfigure or restart GitLab for the changes to take + effect. + +You should now be able to visit your project, and the jobs will start +running again. + [reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source 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. ![GitLab GKE cluster details](img/guide_gitlab_gke_details.png) 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/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md new file mode 100644 index 00000000000..e64ab2acae2 --- /dev/null +++ b/doc/update/11.4-to-11.5.md @@ -0,0 +1,404 @@ +--- +comments: false +--- + +# From 11.4 to 11.5 + +Make sure you view this update guide from the branch (version) of GitLab you would +like to install (e.g., `11-5-stable`. You can select the branch in the version +dropdown at the top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download Ruby and compile it: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz +echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz +cd ruby-2.4.5 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v6.0.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v6.0.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-5-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-5-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update gitlab-pages + +#### Only needed if you use GitLab Pages. + +Install and compile gitlab-pages. GitLab-Pages uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 12. Update configuration files + +#### New `unicorn.rb` configuration + +Note: we have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future. + +- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/unicorn.rb.example but with your settings. + - In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below: + +```ruby +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do |server, worker| + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +after_fork do |server, worker| + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end +``` + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/11-4-stable:config/gitlab.yml.example origin/11-5-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 13. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 14. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 15. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (11.4) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 11.3 to 11.4](11.3-to-11.4.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index 35a9d7adb28..bd0155dc712 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -42,7 +42,11 @@ not send any project names, usernames, or any other specific data. The information from the usage ping is not anonymous, it is linked to the hostname of the instance. -You can view the exact JSON payload in the administration panel. +You can view the exact JSON payload in the administration panel. To view the payload: + +1. Go to the **Admin area** (spanner symbol on the top bar). +1. Expand **Settings** in the left sidebar and click on **Metrics and profiling**. +1. Expand **Usage statistics** and click on the **Preview payload** button. ### Deactivate the usage ping diff --git a/doc/user/markdown.md b/doc/user/markdown.md index f9bdaea185b..96a509c4b21 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -1,17 +1,8 @@ # Markdown -## GitLab Flavored Markdown (GFM) - -> **Note:** -> Not all of the GitLab-specific extensions to Markdown that are described in -> this document currently work on our documentation website. -> -> For the best result, we encourage you to check this document out as rendered -> by GitLab: [markdown.md] - -_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the repositories are also processed with CommonMark. Older content in issues/comments are still processed using the [Redcarpet Ruby library][redcarpet]._ +This markdown guide is valid for GitLab's system markdown entries and files. -_Where there are significant differences, we will try to call them out in this document._ +## GitLab Flavored Markdown (GFM) GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification][commonmark-spec] (which is based on standard Markdown) in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). @@ -26,11 +17,28 @@ You can use GFM in the following areas: - markdown documents inside the repository You can also use other rich text files in GitLab. You might have to install a -dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +dependency to do so. Please see the [`github-markup` gem readme](https://github.com/gitlabhq/markup#markups) for more information. + +> **Notes:** +> +> For the best result, we encourage you to check this document out as rendered +> by GitLab itself: [markdown.md] +> +> As of 11.1, GitLab uses the [CommonMark Ruby Library][commonmarker] for Markdown +processing of all new issues, merge requests, comments, and other Markdown content +in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the +repositories are also processed with CommonMark. Older content in issues/comments +are still processed using the [Redcarpet Ruby library][redcarpet]. +> +> _Where there are significant differences, we will try to call them out in this document._ ### Transitioning to CommonMark -You may have Markdown documents in your repository that were written using some of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a slightly stricter syntax, these documents may now display a little strangely since we've transitioned to CommonMark. Numbered lists with nested lists in particular can be displayed incorrectly. +You may have Markdown documents in your repository that were written using some +of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a +slightly stricter syntax, these documents may now display a little strangely +since we've transitioned to CommonMark. Numbered lists with nested lists in +particular can be displayed incorrectly. It is usually quite easy to fix. In the case of a nested list such as this: @@ -50,11 +58,18 @@ simply add a space to each nested item: In the documentation below, we try to highlight some of the differences. -If you have a need to view a document using RedCarpet, you can add the token `legacy_render=1` to the end of the url, like this: +If you have a need to view a document using RedCarpet, you can add the token +`legacy_render=1` to the end of the url, like this: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md?legacy_render=1 -If you have a large volume of Markdown files, it can be tedious to determine if they will be displayed correctly or not. You can use the [diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and differences between how RedCarpet and CommonMark render the files. It can give you a great idea if anything needs to be changed - many times nothing will need to changed. +If you have a large volume of Markdown files, it can be tedious to determine +if they will be displayed correctly or not. You can use the +[diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) +tool (not an officially supported product) to generate a list of files and +differences between how RedCarpet and CommonMark render the files. It can give +you a great idea if anything needs to be changed - many times nothing will need +to changed. ### Newlines @@ -63,7 +78,8 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline GFM honors the markdown specification in how [paragraphs and line breaks are handled][commonmark-spec]. -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +A paragraph is simply one or more consecutive lines of text, separated by one or +more blank lines. Line-breaks, or soft returns, are rendered if you end a line with two or more spaces: <!-- (Do *NOT* remove the two ending whitespaces in the following line.) --> @@ -85,7 +101,9 @@ Sugar is sweet > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words -It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words: +It is not reasonable to italicize just _part_ of a word, especially when you're +dealing with code and names that often appear with multiple underscores. +Therefore, GFM ignores multiple underscores in words: perform_complicated_task @@ -124,7 +142,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multili On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines, GFM supports multiline blockquotes fenced by <code>>>></code>: -```no-highlight +``` >>> If you paste a message from somewhere else @@ -158,7 +176,7 @@ Blocks of code are either fenced by lines with three back-ticks <code>```</code> or are indented with four spaces. Only the fenced code blocks support syntax highlighting: -```no-highlight +``` Inline `code` has `back-ticks around` it. ``` @@ -248,21 +266,23 @@ However the wrapping tags cannot be mixed as such: > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji - Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: +``` +Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: - :zap: You can use emoji anywhere GFM is supported. :v: +:zap: You can use emoji anywhere GFM is supported. :v: - You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. +You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. - If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. +If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. - Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: - Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. +Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. - On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. +On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. - Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. +Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. +``` Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px">. Well we have a gift for you: @@ -281,7 +301,6 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. - ### Special GitLab References GFM recognizes special references. @@ -343,7 +362,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-li You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so: -```no-highlight +``` - [x] Completed task - [ ] Incomplete task - [ ] Sub-task 1 @@ -355,7 +374,7 @@ You can add task lists to issues, merge requests and comments. To create a task Tasks formatted as ordered lists are supported as well: -```no-highlight +``` 1. [x] Completed task 1. [ ] Incomplete task 1. [ ] Sub-task 1 @@ -412,7 +431,7 @@ This math is inline ![alt text](img/math_inline_sup_render_gfm.png). This is on a separate line -<div align="center"><img src="./img/math_inline_sup_render_gfm.png" ></div> +<img src="./img/math_inline_sup_render_gfm.png" > _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ @@ -440,7 +459,7 @@ Examples: `HSL(540,70%,50%)` `HSLA(540,70%,50%,0.7)` -Become: +Becomes: ![alt color-inline-colorchip-render-gfm](img/color_inline_colorchip_render_gfm.png) @@ -482,7 +501,7 @@ For details see the [Mermaid official page][mermaid]. ### Headers -```no-highlight +``` # H1 ## H2 ### H3 @@ -540,7 +559,7 @@ Note that the Emoji processing happens before the header IDs are generated, so t Examples: -```no-highlight +``` Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. @@ -550,7 +569,7 @@ Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` -Become: +Becomes: Emphasis, aka italics, with *asterisks* or _underscores_. @@ -564,7 +583,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~ Examples: -```no-highlight +``` 1. First ordered list item 2. Another item * Unordered sub-list. @@ -577,7 +596,7 @@ Examples: + Or pluses ``` -Become: +Becomes: 1. First ordered list item 2. Another item @@ -595,7 +614,7 @@ each subsequent paragraph should be indented to the same level as the start of t Example: -```no-highlight +``` 1. First ordered list item Second paragraph of first item. @@ -616,7 +635,7 @@ the paragraph will appear outside the list, instead of properly indented under t Example: -```no-highlight +``` 1. First ordered list item Paragraph of first item. @@ -676,7 +695,7 @@ Examples: [logo]: img/markdown_logo.png -Become: +Becomes: Here's our logo: @@ -694,7 +713,7 @@ Reference-style: Examples: -```no-highlight +``` > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -703,7 +722,7 @@ Quote break. > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. ``` -Become: +Becomes: > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -720,7 +739,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd Examples: -```no-highlight +``` <dl> <dt>Definition list</dt> <dd>Is something people use sometimes.</dd> @@ -730,7 +749,7 @@ Examples: </dl> ``` -Become: +Becomes: <dl> <dt>Definition list</dt> @@ -788,7 +807,7 @@ ___ Underscores ``` -Become: +Becomes: Three or more... @@ -826,7 +845,7 @@ This line is *on its own line*, because the previous line ends with two spaces. spaces. ``` -Become: +Becomes: Here's a line for us to start with. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 48004471f0a..762d254d6cc 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -113,6 +113,14 @@ To add an existing Kubernetes cluster to your project: After a couple of minutes, your cluster will be ready to go. You can now proceed to install some [pre-defined applications](#installing-applications). +To determine the: + +- API URL, run `kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'`. +- Token: + 1. List the secrets by running: `kubectl get secrets`. Note the name of the secret you need the token for. + 1. Get the token for the appropriate secret by running: `kubectl get secret <SECRET_NAME> -o jsonpath="{['data']['token']}" | base64 -D`. +- CA certificate, run `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D`. + ## Security implications CAUTION: **Important:** 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 464fa5987c1..9e2434c02ec 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -176,6 +176,9 @@ 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. +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 **projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**. diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index f9ebf277125..0a7f7d37384 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -236,6 +236,35 @@ all your changes will be available to preview by anyone with the Review Apps lin Find out about [bulk editing merge requests](../../project/bulk_editing.md). +## Troubleshooting + +Sometimes things don't go as expected in a merge request, here are some +troubleshooting steps. + +### Merge request cannot retrieve the pipeline status + +This can occur for one of two reasons: + +* Sidekiq doesn't pick up the changes fast enough +* Because of the bug described in [#41545](https://gitlab.com/gitlab-org/gitlab-ce/issues/41545) + +#### Sidekiq + +Sidekiq didn't process the CI state change fast enough. Please wait a few +seconds and the status will update automatically. + +#### Bug + +Merge Request pipeline statuses can't be retrieved when the following occurs: + +1. A Merge Requst is created +1. The Merge Request is closed +1. Changes are made in the project +1. The Merge Request is reopened + +To enable the pipeline status to be properly retrieved, close and reopen the +Merge Request again. + ## Tips Here are some tips that will help you be more efficient with merge requests in 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**. + +![filter by none any](img/issues_filter_none_any.png) + +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/doc/workflow/notifications.md b/doc/workflow/notifications.md index 731c9209224..9e41038e02e 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -10,31 +10,32 @@ You can find notification settings under the user profile. Notification settings are divided into three groups: -* Global Settings -* Group Settings -* Project Settings +- Global settings +- Group settings +- Project settings Each of these settings have levels of notification: -* Disabled - turns off notifications -* Participating - receive notifications from related resources -* Watch - receive notifications from projects or groups user is a member of -* Global - notifications as set at the global settings -* Custom - user will receive notifications when mentioned, is participant and custom selected events. +- Watch: Receive notifications for any activity. +- On Mention: Receive notifications when `@mentioned` in comments. +- Participate: Receive notifications for threads you have participated in. +- Disabled: Turns off notifications. +- Custom: Receive notifications for custom selected events. +- Global: For groups and projects, notifications as per global settings. -#### Global Settings +### Global Settings -Global Settings are at the bottom of the hierarchy. +Global settings are at the bottom of the hierarchy. Any setting set here will be overridden by a setting at the group or a project level. Group or Project settings can use `global` notification setting which will then use anything that is set at Global Settings. -#### Group Settings +### Group Settings ![notification settings](img/notification_group_settings.png) -Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings: +Group settings are taking precedence over Global Settings but are on a level below Project or Subgroup settings: ``` Group < Subgroup < Project @@ -46,11 +47,11 @@ Organization like this is suitable for users that belong to different groups but same need for being notified for every group they are member of. These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. -#### Project Settings +### Project Settings ![notification settings](img/notification_project_settings.png) -Project Settings are at the top level and any setting placed at this level will take precedence of any +Project settings are at the top level and any setting placed at this level will take precedence of any other setting. This is suitable for users that have different needs for notifications per project basis. These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. @@ -73,11 +74,12 @@ Below is the table of events users can be notified of: ### Issue / Merge request events In most of the below cases, the notification will be sent to: + - Participants: - the author and assignee of the issue/merge request - authors of comments on the issue/merge request - anyone mentioned by `@username` in the issue/merge request title or description - - anyone mentioned by `@username` in any of the comments on the issue/merge request + - anyone mentioned by `@username` in any of the comments on the issue/merge request ...with notification level "Participating" or higher - Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request @@ -129,16 +131,19 @@ Notification emails include headers that provide extra content about the notific | X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc | #### X-GitLab-NotificationReason + This header holds the reason for the notification to have been sent out, where reason can be `mentioned`, `assigned`, `own_activity`, etc. Only one reason is sent out according to its priority: + - `own_activity` - `assigned` - `mentioned` -The reason in this header will also be shown in the footer of the notification email. For example an email with the +The reason in this header will also be shown in the footer of the notification email. For example an email with the reason `assigned` will have this sentence in the footer: `"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"` -**Note: Only reasons listed above have been implemented so far** -Further implementation is [being discussed here](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062) +NOTE: **Note:** +Only reasons listed above have been implemented so far. +Further implementation is [being discussed](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062). diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 18c30723d73..9f7be27b047 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -160,13 +160,27 @@ module API # (fixed in https://github.com/rails/rails/pull/25976). project.tags.map(&:name).sort end + expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url + + expose :license_url, if: :license do |project| + license = project.repository.license_blob + + if license + Gitlab::Routing.url_helpers.project_blob_url(project, File.join(project.default_branch, license.path)) + end + end + + expose :license, with: 'API::Entities::LicenseBasic', if: :license do |project| + project.repository.license + end + expose :avatar_url do |project, options| project.avatar_url(only_path: false) end + expose :star_count, :forks_count expose :last_activity_at - expose :namespace, using: 'API::Entities::NamespaceBasic' expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes @@ -1208,11 +1222,14 @@ module API expose :deployable, using: Entities::Job end - class License < Grape::Entity + class LicenseBasic < Grape::Entity expose :key, :name, :nickname - expose :popular?, as: :popular expose :url, as: :html_url expose(:source_url) { |license| license.meta['source'] } + end + + class License < LicenseBasic + expose :popular?, as: :popular expose(:description) { |license| license.meta['description'] } expose(:conditions) { |license| license.meta['conditions'] } expose(:permissions) { |license| license.meta['permissions'] } diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb index 23b1cd1ad45..1058f4e8a5e 100644 --- a/lib/api/helpers/custom_validators.rb +++ b/lib/api/helpers/custom_validators.rb @@ -10,8 +10,21 @@ module API raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence) end end + + class IntegerNoneAny < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return if value.is_a?(Integer) || + [IssuableFinder::FILTER_NONE, IssuableFinder::FILTER_ANY].include?(value.to_s.downcase) + + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], + message: "should be an integer, 'None' or 'Any'" + end + end end end end Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence) +Grape::Validations.register_validator(:integer_none_any, ::API::Helpers::CustomValidators::IntegerNoneAny) 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/issues.rb b/lib/api/issues.rb index 25d78053c88..e37083165f5 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -8,6 +8,15 @@ module API helpers ::Gitlab::IssuableMetadata + # EE::API::Issues would override the following helpers + helpers do + params :issues_params_ee do + end + + params :issue_params_ee do + end + end + helpers do # rubocop: disable CodeReuse/ActiveRecord def find_issues(args = {}) @@ -40,14 +49,17 @@ module API optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' - optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return issues which are assigned to the user with the given ID' optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' use :pagination + + use :issues_params_ee end - params :issue_params_ce do + params :issue_params do optional :description, type: String, desc: 'The description of an issue' optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue' @@ -56,10 +68,8 @@ module API optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" - end - params :issue_params do - use :issue_params_ce + use :issue_params_ee end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 440d94ae186..a617efaaa4c 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -89,7 +89,8 @@ module API optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time' optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' - optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return merge requests which are assigned to the user with the given ID' optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ae2d327e45b..0a914f9012e 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -114,7 +114,8 @@ module API options = options.reverse_merge( with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, statistics: params[:statistics], - current_user: current_user + current_user: current_user, + license: false ) options[:with] = Entities::BasicProjectDetails if params[:simple] @@ -230,13 +231,17 @@ module API params do use :statistics_params use :with_custom_attributes + + optional :license, type: Boolean, default: false, + desc: 'Include project license data' end get ":id" do options = { with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, current_user: current_user, user_can_admin_project: can?(current_user, :admin_project, user_project), - statistics: params[:statistics] + statistics: params[:statistics], + license: params[:license] } project, options = with_custom_attributes(user_project, options) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index d8768a54986..2f15f3a7d76 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -142,8 +142,7 @@ module API requires :id, type: Integer, desc: %q(Job's ID) optional :trace, type: String, desc: %q(Job's full trace) optional :state, type: String, desc: %q(Job's status: success, failed) - optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys, - desc: %q(Job's failure_reason) + optional :failure_reason, type: String, desc: %q(Job's failure_reason) end put '/:id' do job = authenticate_job! 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/blame.rb b/lib/gitlab/blame.rb index 0d79594363e..f1a653a9d95 100644 --- a/lib/gitlab/blame.rb +++ b/lib/gitlab/blame.rb @@ -43,8 +43,7 @@ module Gitlab def highlighted_lines @blob.load_all_data! - @highlighted_lines ||= - Gitlab::Highlight.highlight(@blob.path, @blob.data, repository: repository).lines + @highlighted_lines ||= @blob.present.highlight.lines end def project 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/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index e780f8c646b..974b5ad6877 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # ANSI color library # # Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code @@ -265,7 +267,7 @@ module Gitlab def reset_state @offset = 0 @n_open_tags = 0 - @out = '' + @out = +'' reset end diff --git a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb index ee3647f24fd..25a82086676 100644 --- a/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb +++ b/lib/gitlab/ci/build/artifacts/adapters/gzip_stream.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb index fa6842cf36a..cf37d700991 100644 --- a/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb +++ b/lib/gitlab/ci/build/artifacts/adapters/raw_stream.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index 551d4f4473e..08dac756cc1 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'zlib' require 'json' diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb index 85072a072d6..d0a80518ae8 100644 --- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb +++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/artifacts/path.rb b/lib/gitlab/ci/build/artifacts/path.rb index 9cd9b36c5f8..65cd935afaa 100644 --- a/lib/gitlab/ci/build/artifacts/path.rb +++ b/lib/gitlab/ci/build/artifacts/path.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/credentials/base.rb b/lib/gitlab/ci/build/credentials/base.rb index 29a7a27c963..58adf6e506d 100644 --- a/lib/gitlab/ci/build/credentials/base.rb +++ b/lib/gitlab/ci/build/credentials/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/credentials/factory.rb b/lib/gitlab/ci/build/credentials/factory.rb index 2423aa8857d..fa805abb8bb 100644 --- a/lib/gitlab/ci/build/credentials/factory.rb +++ b/lib/gitlab/ci/build/credentials/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/credentials/registry.rb b/lib/gitlab/ci/build/credentials/registry.rb index 55eafcaed10..1c8588d9913 100644 --- a/lib/gitlab/ci/build/credentials/registry.rb +++ b/lib/gitlab/ci/build/credentials/registry.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index c811f88f483..4dd932f61d4 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb index d10cc7802d4..43c46ad74af 100644 --- a/lib/gitlab/ci/build/policy.rb +++ b/lib/gitlab/ci/build/policy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb index 782f6c4c0af..4c7dc947cd0 100644 --- a/lib/gitlab/ci/build/policy/kubernetes.rb +++ b/lib/gitlab/ci/build/policy/kubernetes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb index 4aa5dc89f47..10934536536 100644 --- a/lib/gitlab/ci/build/policy/refs.rb +++ b/lib/gitlab/ci/build/policy/refs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb index f09ba42c074..ceb5210cfb5 100644 --- a/lib/gitlab/ci/build/policy/specification.rb +++ b/lib/gitlab/ci/build/policy/specification.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb index 9d2a362b7d4..0698136166a 100644 --- a/lib/gitlab/ci/build/policy/variables.rb +++ b/lib/gitlab/ci/build/policy/variables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb index 0b1ebe4e048..d587c896712 100644 --- a/lib/gitlab/ci/build/step.rb +++ b/lib/gitlab/ci/build/step.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Build diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb index 7b7354bce16..a4f01468e8e 100644 --- a/lib/gitlab/ci/charts.rb +++ b/lib/gitlab/ci/charts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Charts diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index fedaf18ef30..2fb3c4582e7 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci # diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb index e80f9d2e452..ef5f25b42c0 100644 --- a/lib/gitlab/ci/config/entry/artifacts.rb +++ b/lib/gitlab/ci/config/entry/artifacts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb index 3e87a09704e..3c2e1df9b83 100644 --- a/lib/gitlab/ci/config/entry/attributable.rb +++ b/lib/gitlab/ci/config/entry/attributable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb index f3357f85b99..b9639c83075 100644 --- a/lib/gitlab/ci/config/entry/boolean.rb +++ b/lib/gitlab/ci/config/entry/boolean.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb index d7e09acbbf3..0a25057f482 100644 --- a/lib/gitlab/ci/config/entry/cache.rb +++ b/lib/gitlab/ci/config/entry/cache.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb index 9f66f11be9b..d9658291ebe 100644 --- a/lib/gitlab/ci/config/entry/commands.rb +++ b/lib/gitlab/ci/config/entry/commands.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb index 697f622c45e..4aabf0cfa31 100644 --- a/lib/gitlab/ci/config/entry/configurable.rb +++ b/lib/gitlab/ci/config/entry/configurable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 12a063059cb..690409ccf77 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index 0c1f9eb7cbf..07e9e1d3f67 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb index 6be8288748f..85c9c3511a4 100644 --- a/lib/gitlab/ci/config/entry/factory.rb +++ b/lib/gitlab/ci/config/entry/factory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index 04077fa7a61..eba203d9d06 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb index 6fc3aa385bc..dc0ede2a25f 100644 --- a/lib/gitlab/ci/config/entry/hidden.rb +++ b/lib/gitlab/ci/config/entry/hidden.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 2844be80a84..fc453b72fa5 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index f290ff3a565..e4610faa327 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index 96b6f2e5d6c..1535b108000 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb index f27ad0a7759..963b200c7bb 100644 --- a/lib/gitlab/ci/config/entry/key.rb +++ b/lib/gitlab/ci/config/entry/key.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index a3d4432be82..4043629dea9 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb index 26505c91be3..347089722e4 100644 --- a/lib/gitlab/ci/config/entry/node.rb +++ b/lib/gitlab/ci/config/entry/node.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb index 68dad161149..9580b5e2e7f 100644 --- a/lib/gitlab/ci/config/entry/paths.rb +++ b/lib/gitlab/ci/config/entry/paths.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index c92562f8c85..0535d7c1a1a 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb index 29ecd9995ca..f7d39e5cf55 100644 --- a/lib/gitlab/ci/config/entry/script.rb +++ b/lib/gitlab/ci/config/entry/script.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 3e2ebcff31a..47bf9205147 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb index 0066894e069..bdf7f80f382 100644 --- a/lib/gitlab/ci/config/entry/services.rb +++ b/lib/gitlab/ci/config/entry/services.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb index 12764629686..9961bbfaa40 100644 --- a/lib/gitlab/ci/config/entry/simplifiable.rb +++ b/lib/gitlab/ci/config/entry/simplifiable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb index b7afaba1de8..65ab5953131 100644 --- a/lib/gitlab/ci/config/entry/stage.rb +++ b/lib/gitlab/ci/config/entry/stage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb index ec187bd3732..ab184246d29 100644 --- a/lib/gitlab/ci/config/entry/stages.rb +++ b/lib/gitlab/ci/config/entry/stages.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb index 1171ac10f22..77dcfa88170 100644 --- a/lib/gitlab/ci/config/entry/undefined.rb +++ b/lib/gitlab/ci/config/entry/undefined.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb index fbb2551e870..bab32489d2f 100644 --- a/lib/gitlab/ci/config/entry/unspecified.rb +++ b/lib/gitlab/ci/config/entry/unspecified.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb index e45787773a8..08a6593c980 100644 --- a/lib/gitlab/ci/config/entry/validatable.rb +++ b/lib/gitlab/ci/config/entry/validatable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb index 2df23a3edcd..33ffdd3a95d 100644 --- a/lib/gitlab/ci/config/entry/validator.rb +++ b/lib/gitlab/ci/config/entry/validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index f6b4ba7843e..805d26ca8d8 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 8acab605c91..6fd3cec2f5f 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb index 141d2714cb6..b4c491e84a6 100644 --- a/lib/gitlab/ci/config/loader.rb +++ b/lib/gitlab/ci/config/loader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Config diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb index 73f36735e35..b1db9084662 100644 --- a/lib/gitlab/ci/cron_parser.rb +++ b/lib/gitlab/ci/cron_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class CronParser diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb index 0daddaa638c..58d55b1bd6f 100644 --- a/lib/gitlab/ci/mask_secret.rb +++ b/lib/gitlab/ci/mask_secret.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + module Gitlab module Ci::MaskSecret class << self def mask!(value, token) return value unless value.present? && token.present? + # We assume 'value' must be mutable, given + # that frozen string is enabled. value.gsub!(token, 'x' * token.length) value end diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb index 3994a50772b..fbdb84c0522 100644 --- a/lib/gitlab/ci/model.rb +++ b/lib/gitlab/ci/model.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Model 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/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb index efed19da21c..bab1c73e2f1 100644 --- a/lib/gitlab/ci/pipeline/chain/base.rb +++ b/lib/gitlab/ci/pipeline/chain/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index b5eb0cfa2f0..b445a872b3d 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index a53c80d34f7..05978804d92 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -1,4 +1,7 @@ -module Gitlab # rubocop:disable Naming/FileName +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +module Gitlab module Ci module Pipeline module Chain diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index 02493c7fe02..c882241ef6a 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb index bf1380a1da9..6bb3a75291b 100644 --- a/lib/gitlab/ci/pipeline/chain/helpers.rb +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index f34c11ca3c2..633d3cd4f6b 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb index e24630656d3..99780409085 100644 --- a/lib/gitlab/ci/pipeline/chain/sequence.rb +++ b/lib/gitlab/ci/pipeline/chain/sequence.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb index 32cbb7ca6af..b9707d2f8f5 100644 --- a/lib/gitlab/ci/pipeline/chain/skip.rb +++ b/lib/gitlab/ci/pipeline/chain/skip.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb index 13c6fedd831..ebd7e6e8289 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb index a3bd2a5a23a..28c38cc3d18 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/config.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb index 9699c24e5b6..d88851d8245 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb index 30701e1de1b..de24bbf688b 100644 --- a/lib/gitlab/ci/pipeline/duration.rb +++ b/lib/gitlab/ci/pipeline/duration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb index f57df7c5637..61d392121d8 100644 --- a/lib/gitlab/ci/pipeline/expression.rb +++ b/lib/gitlab/ci/pipeline/expression.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb index 047ab66e9b3..70c774416f6 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb index 3a2f0c6924e..668e85f5b9e 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb index 10957598f76..cd17bc4d78b 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb index a2778716924..be7258c201a 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb index f640d0b5855..3ebceb92eb7 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb index 9b239c29ea4..d7e6dacf068 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb index 346c92dc51e..2db2bf011f1 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb index f2611d65faf..ef9ddb6cae9 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb index 37643c8ef53..85c0899e4f6 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index 4cacb1e62c9..f26542361a2 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb index 90f94d0b763..ed184309ab4 100644 --- a/lib/gitlab/ci/pipeline/expression/parser.rb +++ b/lib/gitlab/ci/pipeline/expression/parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb index b36f1e0f865..b03611f756e 100644 --- a/lib/gitlab/ci/pipeline/expression/statement.rb +++ b/lib/gitlab/ci/pipeline/expression/statement.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/expression/token.rb b/lib/gitlab/ci/pipeline/expression/token.rb index 58211800b88..513d43f6fca 100644 --- a/lib/gitlab/ci/pipeline/expression/token.rb +++ b/lib/gitlab/ci/pipeline/expression/token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb index db9706924bb..1fd3a61017f 100644 --- a/lib/gitlab/ci/pipeline/seed/base.rb +++ b/lib/gitlab/ci/pipeline/seed/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 6980b0b7aff..ef738a93bfe 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 2b58d9863a0..4775ff15581 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Pipeline diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb index b4d08ed257f..292e273a03a 100644 --- a/lib/gitlab/ci/reports/test_case.rb +++ b/lib/gitlab/ci/reports/test_case.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Reports diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_reports.rb index c87bdb4a8a2..7397ff35d46 100644 --- a/lib/gitlab/ci/reports/test_reports.rb +++ b/lib/gitlab/ci/reports/test_reports.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Reports diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb index 726c6a11a81..11810bdc0a8 100644 --- a/lib/gitlab/ci/reports/test_reports_comparer.rb +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Reports diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index b5f15397c0f..b0391160c15 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Reports diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb index 642aa593092..9cb7db5934c 100644 --- a/lib/gitlab/ci/reports/test_suite_comparer.rb +++ b/lib/gitlab/ci/reports/test_suite_comparer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci module Reports 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/ci/trace.rb b/lib/gitlab/ci/trace.rb index 93e219a21f9..8eccd262db9 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class Trace diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index a427aa30683..39a1b52e531 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Gitlab module Ci class YamlProcessor diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb new file mode 100644 index 00000000000..b05dca409d1 --- /dev/null +++ b/lib/gitlab/cluster/lifecycle_events.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module Gitlab + module Cluster + # + # LifecycleEvents lets Rails initializers register application startup hooks + # that are sensitive to forking. For example, to defer the creation of + # watchdog threads. This lets us abstract away the Unix process + # lifecycles of Unicorn, Sidekiq, Puma, Puma Cluster, etc. + # + # We have three lifecycle events. + # + # - before_fork (only in forking processes) + # - worker_start + # - before_master_restart (only in forking processes) + # + # Blocks will be executed in the order in which they are registered. + # + class LifecycleEvents + class << self + # + # Hook registration methods (called from initializers) + # + def on_worker_start(&block) + if in_clustered_environment? + # Defer block execution + (@worker_start_hooks ||= []) << block + else + yield + end + end + + def on_before_fork(&block) + return unless in_clustered_environment? + + # Defer block execution + (@before_fork_hooks ||= []) << block + end + + def on_master_restart(&block) + return unless in_clustered_environment? + + # Defer block execution + (@master_restart_hooks ||= []) << block + end + + # + # Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.) + # + def do_worker_start + @worker_start_hooks&.each do |block| + block.call + end + end + + def do_before_fork + @before_fork_hooks&.each do |block| + block.call + end + end + + def do_master_restart + @master_restart_hooks && @master_restart_hooks.each do |block| + block.call + end + end + + # Puma doesn't use singletons (which is good) but + # this means we need to pass through whether the + # puma server is running in single mode or cluster mode + def set_puma_options(options) + @puma_options = options + end + + private + + def in_clustered_environment? + # Sidekiq doesn't fork + return false if Sidekiq.server? + + # Unicorn always forks + return true if defined?(::Unicorn) + + # Puma sometimes forks + return true if in_clustered_puma? + + # Default assumption is that we don't fork + false + end + + def in_clustered_puma? + return false unless defined?(::Puma) + + @puma_options && @puma_options[:workers] && @puma_options[:workers] > 0 + end + end + end + end +end diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb new file mode 100644 index 00000000000..331c39f7d6b --- /dev/null +++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Cluster + class PumaWorkerKillerInitializer + def self.start(puma_options, puma_per_worker_max_memory_mb: 650) + require 'puma_worker_killer' + + PumaWorkerKiller.config do |config| + # Note! ram is expressed in megabytes (whereas GITLAB_UNICORN_MEMORY_MAX is in bytes) + # Importantly RAM is for _all_workers (ie, the cluster), + # not each worker as is the case with GITLAB_UNICORN_MEMORY_MAX + worker_count = puma_options[:workers] || 1 + config.ram = worker_count * puma_per_worker_max_memory_mb + + config.frequency = 20 # seconds + + # We just want to limit to a fixed maximum, unrelated to the total amount + # of available RAM. + config.percent_usage = 0.98 + + # Ideally we'll never hit the maximum amount of memory. If so the worker + # is restarted already, thus periodically restarting workers shouldn't be + # needed. + config.rolling_restart_frequency = false + end + + PumaWorkerKiller.start + end + end + end +end diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 30911b49b18..501c2111530 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -3,6 +3,7 @@ module Gitlab class File include Gitlab::Routing include IconsHelper + include Gitlab::Utils::StrongMemoize CONTEXT_LINES = 3 @@ -30,11 +31,8 @@ module Gitlab end def highlight_lines! - their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") - our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") - - their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines - our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines + their_highlight = Gitlab::Highlight.highlight(their_path, their_lines, language: their_language).lines + our_highlight = Gitlab::Highlight.highlight(our_path, our_lines, language: our_language).lines lines.each do |line| line.rich_text = @@ -182,6 +180,34 @@ module Gitlab raw_line[:line_new], parent_file: self) end end + + def their_language + strong_memoize(:their_language) do + repository.gitattribute(their_path, 'gitlab-language') + end + end + + def our_language + strong_memoize(:our_language) do + if our_path == their_path + their_language + else + repository.gitattribute(our_path, 'gitlab-language') + end + end + end + + def their_lines + strong_memoize(:their_lines) do + lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") + end + end + + def our_lines + strong_memoize(:our_lines) do + lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") + end + end end end end 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/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index a605ddb5c33..1d833183ec3 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -79,7 +79,7 @@ module Gitlab return [] unless blob blob.load_all_data! - Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines + blob.present.highlight.lines end end end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 13b0bb930f4..0bd1d3420a2 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -5,6 +5,7 @@ module Gitlab class Blob include Gitlab::BlobHelper include Gitlab::EncodingHelper + extend Gitlab::Git::WrapsGitalyErrors # This number is the maximum amount of data that we want to display to # the user. We load as much as we can for encoding detection and LFS @@ -75,7 +76,7 @@ module Gitlab # Returns array of Gitlab::Git::Blob # Does not guarantee blob data will be set def batch_lfs_pointers(repository, blob_ids) - repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do repository.gitaly_blob_client.batch_lfs_pointers(blob_ids.to_a) end end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 74cdabfed9d..2820491b65d 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -3,6 +3,7 @@ module Gitlab module Git class Commit include Gitlab::EncodingHelper + extend Gitlab::Git::WrapsGitalyErrors attr_accessor :raw_commit, :head @@ -59,7 +60,7 @@ module Gitlab # This saves us an RPC round trip. return nil if commit_id.include?(':') - commit = repo.wrapped_gitaly_errors do + commit = wrapped_gitaly_errors do repo.gitaly_commit_client.find_commit(commit_id) end @@ -100,7 +101,7 @@ module Gitlab # Commit.between(repo, '29eda46b', 'master') # def between(repo, base, head) - repo.wrapped_gitaly_errors do + wrapped_gitaly_errors do repo.gitaly_commit_client.between(base, head) end end @@ -125,7 +126,7 @@ module Gitlab # are documented here: # http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant) def find_all(repo, options = {}) - repo.wrapped_gitaly_errors do + wrapped_gitaly_errors do Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options) end end @@ -142,7 +143,7 @@ module Gitlab # relation to each other. The last 10 commits for a branch for example, # should go through .where def batch_by_oid(repo, oids) - repo.wrapped_gitaly_errors do + wrapped_gitaly_errors do repo.gitaly_commit_client.list_commits_by_oid(oids) end end diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb index ae6f554bc06..83a9fd5f81a 100644 --- a/lib/gitlab/git/commit_stats.rb +++ b/lib/gitlab/git/commit_stats.rb @@ -3,6 +3,8 @@ module Gitlab module Git class CommitStats + include Gitlab::Git::WrapsGitalyErrors + attr_reader :id, :additions, :deletions, :total # Instantiate a CommitStats object @@ -14,7 +16,7 @@ module Gitlab @deletions = 0 @total = 0 - repo.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_stats(repo, commit) end end diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb index 6dc792c16b8..307f1b8cb66 100644 --- a/lib/gitlab/git/conflict/resolver.rb +++ b/lib/gitlab/git/conflict/resolver.rb @@ -2,6 +2,8 @@ module Gitlab module Git module Conflict class Resolver + include Gitlab::Git::WrapsGitalyErrors + ConflictSideMissing = Class.new(StandardError) ResolutionError = Class.new(StandardError) @@ -12,7 +14,7 @@ module Gitlab end def conflicts - @conflicts ||= @target_repository.wrapped_gitaly_errors do + @conflicts ||= wrapped_gitaly_errors do gitaly_conflicts_client(@target_repository).list_conflict_files.to_a end rescue GRPC::FailedPrecondition => e @@ -22,7 +24,7 @@ module Gitlab end def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:) - source_repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch) 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/remote_mirror.rb b/lib/gitlab/git/remote_mirror.rb index e4743b4db0a..7f9520de5ce 100644 --- a/lib/gitlab/git/remote_mirror.rb +++ b/lib/gitlab/git/remote_mirror.rb @@ -1,13 +1,15 @@ module Gitlab module Git class RemoteMirror + include Gitlab::Git::WrapsGitalyErrors + def initialize(repository, ref_name) @repository = repository @ref_name = ref_name end def update(only_branches_matching: []) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do @repository.gitaly_remote_client.update_remote_mirror(@ref_name, only_branches_matching) end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 9df04372cc2..fcc92341c40 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -6,6 +6,7 @@ module Gitlab module Git class Repository include Gitlab::Git::RepositoryMirroring + include Gitlab::Git::WrapsGitalyErrors include Gitlab::EncodingHelper include Gitlab::Utils::StrongMemoize @@ -845,23 +846,9 @@ module Gitlab end def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block) - Gitlab::GitalyClient.migrate(method, status: status, &block) - rescue GRPC::NotFound => e - raise NoRepository.new(e) - rescue GRPC::InvalidArgument => e - raise ArgumentError.new(e) - rescue GRPC::BadStatus => e - raise CommandError.new(e) - end - - def wrapped_gitaly_errors(&block) - yield block - rescue GRPC::NotFound => e - raise NoRepository.new(e) - rescue GRPC::InvalidArgument => e - raise ArgumentError.new(e) - rescue GRPC::BadStatus => e - raise CommandError.new(e) + wrapped_gitaly_errors do + Gitlab::GitalyClient.migrate(method, status: status, &block) + end end def clean_stale_repository_files diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index e0867aeb5a7..b5b701699f0 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -2,6 +2,7 @@ module Gitlab module Git class Tree include Gitlab::EncodingHelper + extend Gitlab::Git::WrapsGitalyErrors attr_accessor :id, :root_id, :name, :path, :flat_path, :type, :mode, :commit_id, :submodule_url @@ -15,7 +16,7 @@ module Gitlab def where(repository, sha, path = nil, recursive = false) path = nil if path == '' || path == '/' - repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive) end end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 7fe56979d5c..02c643d0da0 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -1,6 +1,8 @@ module Gitlab module Git class Wiki + include Gitlab::Git::WrapsGitalyErrors + DuplicatePageError = Class.new(StandardError) OperationError = Class.new(StandardError) @@ -65,37 +67,37 @@ module Gitlab end def write_page(name, format, content, commit_details) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_write_page(name, format, content, commit_details) end end def delete_page(page_path, commit_details) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_delete_page(page_path, commit_details) end end def update_page(page_path, title, format, content, commit_details) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_update_page(page_path, title, format, content, commit_details) end end def pages(limit: 0) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_get_all_pages(limit: limit) end end def page(title:, version: nil, dir: nil) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_find_page(title: title, version: version, dir: dir) end end def file(name, version) - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_find_file(name, version) end end @@ -105,7 +107,7 @@ module Gitlab # :per_page - The number of items per page. # :limit - Total number of items to return. def page_versions(page_path, options = {}) - versions = @repository.wrapped_gitaly_errors do + versions = wrapped_gitaly_errors do gitaly_wiki_client.page_versions(page_path, options) end @@ -127,7 +129,7 @@ module Gitlab def page_formatted_data(title:, dir: nil, version: nil) version = version&.id - @repository.wrapped_gitaly_errors do + wrapped_gitaly_errors do gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version) end end diff --git a/lib/gitlab/git/wraps_gitaly_errors.rb b/lib/gitlab/git/wraps_gitaly_errors.rb new file mode 100644 index 00000000000..4b161f7e6ce --- /dev/null +++ b/lib/gitlab/git/wraps_gitaly_errors.rb @@ -0,0 +1,15 @@ +module Gitlab + module Git + module WrapsGitalyErrors + def wrapped_gitaly_errors(&block) + yield block + rescue GRPC::NotFound => e + raise Gitlab::Git::Repository::NoRepository.new(e) + rescue GRPC::InvalidArgument => e + raise ArgumentError.new(e) + rescue GRPC::BadStatus => e + raise Gitlab::Git::CommandError.new(e) + end + end + end +end 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/highlight.rb b/lib/gitlab/highlight.rb index 83095acc528..a4e60bbd828 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -4,22 +4,25 @@ module Gitlab class Highlight TIMEOUT_BACKGROUND = 30.seconds TIMEOUT_FOREGROUND = 3.seconds + MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte - def self.highlight(blob_name, blob_content, repository: nil, plain: false) - new(blob_name, blob_content, repository: repository) + def self.highlight(blob_name, blob_content, language: nil, plain: false) + new(blob_name, blob_content, language: language) .highlight(blob_content, continue: false, plain: plain) end attr_reader :blob_name - def initialize(blob_name, blob_content, repository: nil) + def initialize(blob_name, blob_content, language: nil) @formatter = Rouge::Formatters::HTMLGitlab - @repository = repository + @language = language @blob_name = blob_name @blob_content = blob_content end def highlight(text, continue: true, plain: false) + plain ||= text.length > MAXIMUM_TEXT_HIGHLIGHT_SIZE + highlighted_text = highlight_text(text, continue: continue, plain: plain) highlighted_text = link_dependencies(text, highlighted_text) if blob_name highlighted_text @@ -36,11 +39,9 @@ module Gitlab private def custom_language - language_name = @repository && @repository.gitattribute(@blob_name, 'gitlab-language') - - return nil unless language_name + return nil unless @language - Rouge::Lexer.find_fancy(language_name) + Rouge::Lexer.find_fancy(@language) end def highlight_text(text, continue: true, plain: false) 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/patch/draw_route.rb b/lib/gitlab/patch/draw_route.rb new file mode 100644 index 00000000000..b00244a6e04 --- /dev/null +++ b/lib/gitlab/patch/draw_route.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# We're patching `ActionDispatch::Routing::Mapper` in +# config/initializers/routing_draw.rb +module Gitlab + module Patch + module DrawRoute + RoutesNotFound = Class.new(StandardError) + + def draw(routes_name) + drawn_any = draw_ce(routes_name) | draw_ee(routes_name) + + drawn_any || raise(RoutesNotFound.new("Cannot find #{routes_name}")) + end + + def draw_ce(routes_name) + draw_route(route_path("config/routes/#{routes_name}.rb")) + end + + def draw_ee(_) + true + end + + def route_path(routes_name) + Rails.root.join(routes_name) + end + + def draw_route(path) + if File.exist?(path) + instance_eval(File.read(path)) + true + else + false + end + end + end + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 3a202d915e3..04df881bf03 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -82,7 +82,7 @@ module Gitlab ref: ref, startline: startline, data: data.join, - project_id: project ? project.id : nil + project: project ) end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 5ce3eda2ccb..458737f31eb 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -4,8 +4,10 @@ module Gitlab class SearchResults class FoundBlob include EncodingHelper + include Presentable + include BlobLanguageFromGitAttributes - attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id + attr_reader :id, :filename, :basename, :ref, :startline, :data, :project def initialize(opts = {}) @id = opts.fetch(:id, nil) @@ -15,6 +17,11 @@ module Gitlab @startline = opts.fetch(:startline, nil) @data = encode_utf8(opts.fetch(:data, nil)) @per_page = opts.fetch(:per_page, 20) + @project = opts.fetch(:project, nil) + # Some caller does not have project object (e.g. elastic search), + # yet they can trigger many calls in one go, + # causing duplicated queries. + # Allow those to just pass project_id instead. @project_id = opts.fetch(:project_id, nil) end @@ -22,8 +29,12 @@ module Gitlab filename end - def no_highlighting? - false + def project_id + @project_id || @project&.id + end + + def present + super(presenter_class: BlobPresenter) end end 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/gitlab/ldap.rake b/lib/tasks/gitlab/ldap.rake index c66a2a263dc..0459de27c96 100644 --- a/lib/tasks/gitlab/ldap.rake +++ b/lib/tasks/gitlab/ldap.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :ldap do desc 'GitLab | LDAP | Rename provider' - task :rename_provider, [:old_provider, :new_provider] => :environment do |_, args| + task :rename_provider, [:old_provider, :new_provider] => :gitlab_environment do |_, args| old_provider = args[:old_provider] || prompt('What is the old provider? Ex. \'ldapmain\': '.color(:blue)) new_provider = args[:new_provider] || 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..324e5315821 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 "" @@ -1983,6 +1986,9 @@ msgstr "" msgid "Copy token to clipboard" msgstr "" +msgid "Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}" +msgstr "" + msgid "Create" msgstr "" @@ -2324,6 +2330,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 +2762,9 @@ msgstr "" msgid "Failed to check related branches." msgstr "" +msgid "Failed to deploy to" +msgstr "" + msgid "Failed to load emoji list." msgstr "" @@ -3002,9 +3017,15 @@ msgstr "" msgid "Group Runners" msgstr "" +msgid "Group URL" +msgstr "" + msgid "Group avatar" msgstr "" +msgid "Group description" +msgstr "" + msgid "Group description (optional)" msgstr "" @@ -4030,9 +4051,6 @@ msgstr "" msgid "No" msgstr "" -msgid "No Assignee" -msgstr "" - msgid "No Label" msgstr "" @@ -4479,15 +4497,24 @@ msgstr "" msgid "Pipelines|This project is not currently set up to run pipelines." msgstr "" +msgid "Pipeline|Commit" +msgstr "" + msgid "Pipeline|Create for" msgstr "" msgid "Pipeline|Create pipeline" msgstr "" +msgid "Pipeline|Duration" +msgstr "" + msgid "Pipeline|Existing branch name or tag" msgstr "" +msgid "Pipeline|Pipeline" +msgstr "" + msgid "Pipeline|Run Pipeline" msgstr "" @@ -4497,6 +4524,12 @@ msgstr "" msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default." msgstr "" +msgid "Pipeline|Stages" +msgstr "" + +msgid "Pipeline|Status" +msgstr "" + msgid "Pipeline|Stop pipeline" msgstr "" @@ -4533,12 +4566,18 @@ msgstr "" msgid "Please accept the Terms of Service before continuing." msgstr "" +msgid "Please choose a group URL with no special characters." +msgstr "" + msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again." msgstr "" msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again." msgstr "" +msgid "Please fill in a descriptive name for your group." +msgstr "" + msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access." msgstr "" @@ -4890,6 +4929,9 @@ msgstr "" msgid "Projects shared with %{group_name}" msgstr "" +msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group." +msgstr "" + msgid "ProjectsDropdown|Frequently visited" msgstr "" @@ -5607,6 +5649,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 +5829,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 "" @@ -6771,6 +6822,9 @@ msgstr "" msgid "View replaced file @ " msgstr "" +msgid "View the documentation" +msgstr "" + msgid "Visibility and access controls" msgstr "" @@ -6822,6 +6876,9 @@ msgstr "" msgid "Who can see this group?" msgstr "" +msgid "Who will be able to see this group?" +msgstr "" + msgid "Wiki" msgstr "" @@ -6996,9 +7053,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 +7074,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..608d4e58dd3 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.10.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-loader": "^8.0.4", @@ -39,8 +39,6 @@ module QA module Factory autoload :ApiFabricator, 'qa/factory/api_fabricator' autoload :Base, 'qa/factory/base' - autoload :Dependency, 'qa/factory/dependency' - autoload :Product, 'qa/factory/product' module Resource autoload :Sandbox, 'qa/factory/resource/sandbox' @@ -54,7 +52,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' @@ -100,7 +98,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' @@ -183,7 +182,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/README.md b/qa/qa/factory/README.md index 10140e39510..42077f60611 100644 --- a/qa/qa/factory/README.md +++ b/qa/qa/factory/README.md @@ -26,11 +26,7 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - def initialize(name) - @name = name - end + attr_accessor :name def fabricate! Page::Dashboard::Index.perform do |dashboard_index| @@ -64,21 +60,10 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - def initialize(name) - @name = name - end + attr_accessor :name def fabricate! - Page::Dashboard::Index.perform do |dashboard_index| - dashboard_index.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end + # ... same as before end def api_get_path @@ -103,33 +88,31 @@ end The [`Project` factory](./resource/project.rb) is a good real example of Browser UI and API implementations. -### Define dependencies +#### Resource attributes -A resource may need an other resource to exist first. For instance, a project +A resource may need another resource to exist first. For instance, a project needs a group to be created in. -To define a dependency, you can use the `dependency` DSL method. -The first argument is a factory class, then you should pass `as: <name>` to give -a name to the dependency. -That will allow access to the dependency from your resource object's methods. -You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`, -`#api_post_body`. +To define a resource attribute, you can use the `attribute` method with a +block using the other factory to fabricate the resource. + +That will allow access to the other resource from your resource object's +methods. You would usually use it in `#fabricate!`, `#api_get_path`, +`#api_post_path`, `#api_post_body`. -Let's take the `Shirt` factory, and add a `project` dependency to it: +Let's take the `Shirt` factory, and add a `project` attribute to it: ```ruby module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + attr_accessor :name - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end - - def initialize(name) - @name = name + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end end def fabricate! @@ -164,19 +147,19 @@ module QA end ``` -**Note that dependencies are always built via the API fabrication method if -supported by their factories.** +**Note that all the attributes are lazily constructed. This means if you want +a specific attribute to be fabricated first, you'll need to call the +attribute method first even if you're not using it.** -### Define attributes on the created resource +#### Product data attributes Once created, you may want to populate a resource with attributes that can be found in the Web page, or in the API response. For instance, once you create a project, you may want to store its repository SSH URL as an attribute. -To define an attribute, you can use the `product` DSL method. -The first argument is the attribute name, then you should define a name for the -dependency to be accessible from your resource object's methods. +Again we could use the `attribute` method with a block, using a page object +to retrieve the data on the page. Let's take the `Shirt` factory, and define a `:brand` attribute: @@ -185,22 +168,74 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + attr_accessor :name - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end end # Attribute populated from the Browser UI (using the block) - product :brand do + attribute :brand do Page::Shirt::Show.perform do |shirt_show| shirt_show.fetch_brand_from_page end end - def initialize(name) - @name = name - end + # ... same as before + end + end + end +end +``` + +**Note again that all the attributes are lazily constructed. This means if +you call `shirt.brand` after moving to the other page, it'll not properly +retrieve the data because we're no longer on the expected page.** + +Consider this: + +```ruby +shirt = + QA::Factory::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.project.visit! + +shirt.brand # => FAIL! +``` + +The above example will fail because now we're on the project page, trying to +construct the brand data from the shirt page, however we moved to the project +page already. There are two ways to solve this, one is that we could try to +retrieve the brand before visiting the project again: + +```ruby +shirt = + QA::Factory::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.brand # => OK! + +shirt.project.visit! + +shirt.brand # => OK! +``` + +The attribute will be stored in the instance therefore all the following calls +will be fine, using the data previously constructed. If we think that this +might be too brittle, we could eagerly construct the data right before +ending fabrication: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + # ... same as before def fabricate! project.visit! @@ -213,20 +248,8 @@ module QA shirt_new.set_name(name) shirt_new.create_shirt! end - end - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } + populate(:brand) # Eagerly construct the data end end end @@ -234,74 +257,51 @@ module QA end ``` -#### Inherit a factory's attribute +The `populate` method will iterate through its arguments and call each +attribute respectively. Here `populate(:brand)` has the same effect as +just `brand`. Using the populate method makes the intention clearer. -Sometimes, you want a resource to inherit its factory attributes. For instance, -it could be useful to pass the `size` attribute from the `Shirt` factory to the -created resource. -You can do that by defining `product :attribute_name` without a block. +With this, it will make sure we construct the data right after we create the +shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data. -Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes: +Alternatively, we could just make sure we're on the right page before +constructing the brand data: ```ruby module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end + attr_accessor :name - # Attribute from the Browser UI (using the block) - product :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' end end - # Attribute inherited from the Shirt factory if present, - # or a QA::Factory::Product::NoValueError is raised otherwise - product :name - product :size - - def initialize(name) - @name = name - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end + # Attribute populated from the Browser UI (using the block) + attribute :brand do + back_url = current_url + visit! - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page end - end - def api_get_path - "/project/#{project.path}/shirt/#{name}" + visit(back_url) end - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end + # ... same as before end end end end ``` +This will make sure it's on the shirt page before constructing brand, and +move back to the previous page to avoid breaking the state. + #### Define an attribute based on an API response Sometimes, you want to define a resource attribute based on the API response @@ -311,7 +311,6 @@ the API returns ```ruby { brand: 'a-brand-new-brand', - size: 'extra-small', style: 't-shirt', materials: [[:cotton, 80], [:polyamide, 20]] } @@ -320,18 +319,6 @@ the API returns you may want to store `style` as-is in the resource, and fetch the first value of the first `materials` item in a `main_fabric` attribute. -For both attributes, you will need to define an inherited attribute, as shown -in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you -will need to implement the -`#transform_api_resource` method to first populate the `:main_fabric` key in the -API response so that it can be used later to automatically populate the -attribute on your resource. - -If an attribute can only be retrieved from the API response, you should define -a block to give it a default value, otherwise you could get a -`QA::Factory::Product::NoValueError` when creating your resource via the -Browser UI. - Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` attributes: @@ -340,69 +327,21 @@ module QA module Factory module Resource class Shirt < Factory::Base - attr_accessor :name, :size + # ... same as before - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-create-a-shirt' - end + # Attribute from the Shirt factory if present, + # or fetched from the API response if present, + # or a QA::Factory::Base::NoValueError is raised otherwise + attribute :style - # Attribute fetched from the API response if present, - # or from the Browser UI otherwise (using the block) - product :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end + # If the attribute from the Shirt factory is not present, + # and if the API does not contain this field, this block will be + # used to construct the value based on the API response. + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) end - # Attribute fetched from the API response if present, - # or from the Shirt factory if present, - # or a QA::Factory::Product::NoValueError is raised otherwise - product :name - product :size - product :style do - 'unknown' - end - product :main_fabric do - 'unknown' - end - - def initialize(name) - @name = name - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end - - private - - def transform_api_resource(api_response) - api_response[:main_fabric] = api_response[:materials][0][0] - api_response - end + # ... same as before end end end @@ -411,11 +350,10 @@ end **Notes on attributes precedence:** +- factory instance variables have the highest precedence - attributes from the API response take precedence over attributes from the - Browser UI -- attributes from the Browser UI take precedence over attributes from the - factory (i.e inherited) -- attributes without a value will raise a `QA::Factory::Product::NoValueError` error + block (usually from Browser UI) +- attributes without a value will raise a `QA::Factory::Base::NoValueError` error ## Creating resources in your tests @@ -428,42 +366,40 @@ Here is an example that will use the API fabrication method under the hood since it's supported by the `Shirt` factory: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' end +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute -expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response -expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block ``` -If you explicitely want to use the Browser UI fabrication method, you can call +If you explicitly want to use the Browser UI fabrication method, you can call the `.fabricate_via_browser_ui!` method instead: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt| + shirt.name = 'my-shirt' end -expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute -expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute -expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block -expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block +expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) ``` -You can also explicitely use the API fabrication method, by calling the +You can also explicitly use the API fabrication method, by calling the `.fabricate_via_api!` method: ```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt| - shirt.size = 'small' +my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' end ``` -In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`. +In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`. ## Where to ask for help? diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index e1dc23d350d..e28a00c545b 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -10,13 +10,46 @@ module QA include ApiFabricator extend Capybara::DSL - def_delegators :evaluator, :dependency, :dependencies - def_delegators :evaluator, :product, :attributes + NoValueError = Class.new(RuntimeError) + + def_delegators :evaluator, :attribute def fabricate!(*_args) raise NotImplementedError end + def visit! + visit(web_url) + end + + def populate(*attributes) + attributes.each(&method(:public_send)) + end + + private + + def populate_attribute(name, block) + value = attribute_value(name, block) + + raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value + + value + end + + def attribute_value(name, block) + api_value = api_resource&.dig(name) + + if api_value && block + log_having_both_api_result_and_block(name, api_value) + end + + api_value || (block && instance_exec(&block)) + end + + def log_having_both_api_result_and_block(name, api_value) + QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored." + end + def self.fabricate!(*args, &prepare_block) fabricate_via_api!(*args, &prepare_block) rescue NotImplementedError @@ -52,13 +85,10 @@ module QA def self.do_fabricate!(factory:, prepare_block:, parents: []) prepare_block.call(factory) if prepare_block - dependencies.each do |signature| - Factory::Dependency.new(factory, signature).build!(parents: parents + [self]) - end - resource_web_url = yield + factory.web_url = resource_web_url - Factory::Product.populate!(factory, resource_web_url) + factory end private_class_method :do_fabricate! @@ -85,31 +115,40 @@ module QA end private_class_method :evaluator - class DSL - attr_reader :dependencies, :attributes + def self.dynamic_attributes + const_get(:DynamicAttributes) + rescue NameError + mod = const_set(:DynamicAttributes, Module.new) + + include mod + + mod + end + def self.attributes_names + dynamic_attributes.instance_methods(false).sort.grep_v(/=$/) + end + + class DSL def initialize(base) @base = base - @dependencies = [] - @attributes = [] end - def dependency(factory, as:, &block) - as.tap do |name| - @base.class_eval { attr_accessor name } + def attribute(name, &block) + @base.dynamic_attributes.module_eval do + attr_writer(name) - Dependency::Signature.new(name, factory, block).tap do |signature| - @dependencies << signature + define_method(name) do + instance_variable_get("@#{name}") || + instance_variable_set( + "@#{name}", + populate_attribute(name, block)) end end end - - def product(attribute, &block) - Product::Attribute.new(attribute, block).tap do |signature| - @attributes << signature - end - end end + + attribute :web_url end end end diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb deleted file mode 100644 index 655e2677db0..00000000000 --- a/qa/qa/factory/dependency.rb +++ /dev/null @@ -1,28 +0,0 @@ -module QA - module Factory - class Dependency - Signature = Struct.new(:name, :factory, :block) - - def initialize(caller_factory, dependency_signature) - @caller_factory = caller_factory - @dependency_signature = dependency_signature - end - - def overridden? - !!@caller_factory.public_send(@dependency_signature.name) - end - - def build!(parents: []) - return if overridden? - - dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory| - @dependency_signature.block&.call(factory, @caller_factory) - end - - dependency.tap do |dependency| - @caller_factory.public_send("#{@dependency_signature.name}=", dependency) - end - end - end - end -end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb deleted file mode 100644 index 17fe908eaa2..00000000000 --- a/qa/qa/factory/product.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'capybara/dsl' - -module QA - module Factory - class Product - include Capybara::DSL - - NoValueError = Class.new(RuntimeError) - - attr_reader :factory, :web_url - - Attribute = Struct.new(:name, :block) - - def initialize(factory, web_url) - @factory = factory - @web_url = web_url - - populate_attributes! - end - - def visit! - visit(web_url) - end - - def self.populate!(factory, web_url) - new(factory, web_url) - end - - private - - def populate_attributes! - factory.class.attributes.each do |attribute| - instance_exec(factory, attribute.block) do |factory, block| - value = attribute_value(attribute, block) - - raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value - - define_singleton_method(attribute.name) { value } - end - end - end - - def attribute_value(attribute, block) - factory.api_resource&.dig(attribute.name) || - (block && block.call(factory)) || - (factory.respond_to?(attribute.name) && factory.public_send(attribute.name)) - end - end - end -end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb index 6f878396f0e..272b7fc5818 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/factory/repository/project_push.rb @@ -2,14 +2,13 @@ module QA module Factory module Repository class ProjectPush < Factory::Repository::Push - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-code' - project.description = 'Project with repository' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-code' + resource.description = 'Project with repository' + end end - product :output - product :project - def initialize @file_name = 'file.txt' @file_content = '# This is test project' 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/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb index ecc6cc18c88..25b6ffe8323 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/factory/repository/wiki_push.rb @@ -2,10 +2,12 @@ module QA module Factory module Repository class WikiPush < Factory::Repository::Push - dependency Factory::Resource::Wiki, as: :wiki do |wiki| - wiki.title = 'Home' - wiki.content = '# My First Wiki Content' - wiki.message = 'Update home' + attribute :wiki do + Factory::Resource::Wiki.fabricate! do |resource| + resource.title = 'Home' + resource.content = '# My First Wiki Content' + resource.message = 'Update home' + end end def initialize diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb index f3b52565d17..b05d1e252ec 100644 --- a/qa/qa/factory/resource/branch.rb +++ b/qa/qa/factory/resource/branch.rb @@ -5,8 +5,10 @@ module QA attr_accessor :project, :branch_name, :allow_to_push, :allow_to_merge, :protected - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'protected-branch-project' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end end def initialize @@ -43,9 +45,7 @@ module QA # to `allow_to_push` variable. return branch unless @protected - Page::Project::Menu.act do - click_repository_settings - end + Page::Project::Menu.perform(&:click_repository_settings) Page::Project::Settings::Repository.perform do |setting| setting.expand_protected_branches do |page| diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb new file mode 100644 index 00000000000..a0aefc61f9f --- /dev/null +++ b/qa/qa/factory/resource/ci_variable.rb @@ -0,0 +1,30 @@ +module QA + module Factory + module Resource + class CiVariable < Factory::Base + attr_accessor :key, :value + + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Page::Project::Settings::CICD.perform do |setting| + setting.expand_ci_variables do |page| + page.fill_variable(key, value) + + page.save_variables + end + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index 4c53c500c27..aea99c9f80d 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -4,11 +4,11 @@ module QA class DeployKey < Factory::Base attr_accessor :title, :key - product :fingerprint do |resource| - Page::Project::Settings::Repository.act do - expand_deploy_keys do |key| - key_offset = key.key_titles.index do |title| - title.text == resource.title + attribute :fingerprint do + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |key| + key_offset = key.key_titles.index do |key_title| + key_title.text == title end key.key_fingerprints[key_offset].text @@ -16,17 +16,17 @@ module QA end end - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-deploy' - project.description = 'project for adding deploy key test' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy key test' + end end def fabricate! project.visit! - Page::Project::Menu.act do - click_repository_settings - end + Page::Project::Menu.perform(&:click_repository_settings) Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |page| diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb index 159f79ac50b..68e98f0aa01 100644 --- a/qa/qa/factory/resource/deploy_token.rb +++ b/qa/qa/factory/resource/deploy_token.rb @@ -4,25 +4,27 @@ module QA class DeployToken < Factory::Base attr_accessor :name, :expires_at - product :username do |resource| - Page::Project::Settings::Repository.act do - expand_deploy_tokens do |token| + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| token.token_username end end end - product :password do |password| - Page::Project::Settings::Repository.act do - expand_deploy_tokens do |token| + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| token.token_password end end end - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-deploy' - project.description = 'project for adding deploy token test' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end end def fabricate! diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb index f8dea06d361..1148876c2d3 100644 --- a/qa/qa/factory/resource/file.rb +++ b/qa/qa/factory/resource/file.rb @@ -8,8 +8,10 @@ module QA :content, :commit_message - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-new-file' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end end def initialize @@ -21,7 +23,7 @@ module QA def fabricate! project.visit! - Page::Project::Show.act { create_new_file! } + Page::Project::Show.perform(&:create_new_file!) Page::File::Form.perform do |page| page.add_name(@name) diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 6e2a668df64..b1e874af893 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -2,17 +2,19 @@ module QA module Factory module Resource class Fork < Factory::Base - dependency Factory::Repository::ProjectPush, as: :push + attribute :push do + Factory::Repository::ProjectPush.fabricate! + end - dependency Factory::Resource::User, as: :user do |user| - if Runtime::Env.forker? - user.username = Runtime::Env.forker_username - user.password = Runtime::Env.forker_password + attribute :user do + Factory::Resource::User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end end end - product :user - def visit_project_with_retry # The user intermittently fails to stay signed in after visiting the # project page. The new user is registered and then signs in and a @@ -48,15 +50,19 @@ module QA end def fabricate! + populate(:push, :user) + visit_project_with_retry - Page::Project::Show.act { fork_project } + Page::Project::Show.perform(&:fork_project) Page::Project::Fork::New.perform do |fork_new| fork_new.choose_namespace(user.name) end - Page::Layout::Banner.act { has_notice?('The project was successfully forked.') } + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end end end end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb index 2688328df92..45e49da86f9 100644 --- a/qa/qa/factory/resource/group.rb +++ b/qa/qa/factory/resource/group.rb @@ -4,12 +4,12 @@ module QA class Group < Factory::Base attr_accessor :path, :description - dependency Factory::Resource::Sandbox, as: :sandbox - - product :id do - true # We don't retrieve the Group ID when using the Browser UI + attribute :sandbox do + Factory::Resource::Sandbox.fabricate! end + attribute :id + def initialize @path = Runtime::Namespace.name @description = "QA test run at #{Runtime::Namespace.time}" diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb index 9b444cb0bf1..3a28e0d5aa6 100644 --- a/qa/qa/factory/resource/issue.rb +++ b/qa/qa/factory/resource/issue.rb @@ -2,22 +2,21 @@ module QA module Factory module Resource class Issue < Factory::Base - attr_accessor :title, :description, :project + attr_writer :description - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-issues' - project.description = 'project for adding issues' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end end - product :project - product :title + attribute :title def fabricate! project.visit! - Page::Project::Show.act do - go_to_new_issue - end + Page::Project::Show.perform(&:go_to_new_issue) Page::Project::Issue::New.perform do |page| page.add_title(@title) diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb index cdee35c54e3..aac6864f42f 100644 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ b/qa/qa/factory/resource/kubernetes_cluster.rb @@ -7,24 +7,21 @@ module QA attr_writer :project, :cluster, :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - product :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform do |page| - page.ingress_ip - end + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) end def fabricate! @project.visit! - Page::Project::Menu.act { click_operations_kubernetes } + Page::Project::Menu.perform( + &:click_operations_kubernetes) - Page::Project::Operations::Kubernetes::Index.perform do |page| - page.add_kubernetes_cluster - end + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) - Page::Project::Operations::Kubernetes::Add.perform do |page| - page.add_existing_cluster - end + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) Page::Project::Operations::Kubernetes::AddExisting.perform do |page| page.set_cluster_name(@cluster.cluster_name) diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb index 4080f15bf66..32bc519b48c 100644 --- a/qa/qa/factory/resource/label.rb +++ b/qa/qa/factory/resource/label.rb @@ -4,14 +4,14 @@ module QA module Factory module Resource class Label < Factory::Base - attr_accessor :title, - :description, - :color + attr_accessor :description, :color - product(:title) { |factory| factory.title } + attribute :title - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-label' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-label' + end end def initialize @@ -23,8 +23,8 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act { go_to_labels } - Page::Label::Index.act { go_to_new_label } + Page::Project::Menu.perform(&:go_to_labels) + Page::Label::Index.perform(&:go_to_new_label) Page::Label::New.perform do |page| page.fill_title(@title) diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index d30da8a3db0..4b7d2287f98 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -12,27 +12,31 @@ module QA :milestone, :labels - product :project - product :source_branch - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-merge-request' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end end - dependency Factory::Repository::ProjectPush, as: :target do |push, factory| - factory.project.visit! - push.project = factory.project - push.branch_name = 'master' - push.remote_branch = factory.target_branch + attribute :target do + project.visit! + + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end end - dependency Factory::Repository::ProjectPush, as: :source do |push, factory| - push.project = factory.project - push.branch_name = factory.target_branch - push.remote_branch = factory.source_branch - push.new_branch = false - push.file_name = "added_file.txt" - push.file_content = "File Added" + attribute :source do + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = target_branch + resource.remote_branch = source_branch + resource.new_branch = false + resource.file_name = "added_file.txt" + resource.file_content = "File Added" + end end def initialize @@ -46,8 +50,10 @@ module QA end def fabricate! + populate(:target, :source) + project.visit! - Page::Project::Show.act { new_merge_request } + Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform do |page| page.fill_title(@title) page.fill_description(@description) diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb index 6caaf65f673..1311bf625a6 100644 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ b/qa/qa/factory/resource/merge_request_from_fork.rb @@ -4,19 +4,26 @@ module QA class MergeRequestFromFork < MergeRequest attr_accessor :fork_branch - dependency Factory::Resource::Fork, as: :fork + attribute :fork do + Factory::Resource::Fork.fabricate! + end - dependency Factory::Repository::ProjectPush, as: :push do |push, factory| - push.project = factory.fork - push.branch_name = factory.fork_branch - push.file_name = 'file2.txt' - push.user = factory.fork.user + attribute :push do + Factory::Repository::ProjectPush.fabricate! do |resource| + resource.project = fork + resource.branch_name = fork_branch + resource.file_name = 'file2.txt' + resource.user = fork.user + end end def fabricate! + populate(:push) + fork.visit! - Page::Project::Show.act { new_merge_request } - Page::MergeRequest::New.act { create_merge_request } + + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) end end end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb index 166054cfcdc..ceb0f1c3d75 100644 --- a/qa/qa/factory/resource/personal_access_token.rb +++ b/qa/qa/factory/resource/personal_access_token.rb @@ -7,13 +7,13 @@ module QA class PersonalAccessToken < Factory::Base attr_accessor :name - product :access_token do - Page::Profile::PersonalAccessTokens.act { created_access_token } + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) end def fabricate! - Page::Main::Menu.act { go_to_profile_settings } - Page::Profile::Menu.act { click_access_tokens } + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) Page::Profile::PersonalAccessTokens.perform do |page| page.fill_token_name(name || 'api-test-token') diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb index 105e42b23ec..f691ae5a342 100644 --- a/qa/qa/factory/resource/project.rb +++ b/qa/qa/factory/resource/project.rb @@ -4,25 +4,24 @@ module QA module Factory module Resource class Project < Factory::Base - attr_accessor :description - attr_reader :name + attribute :name + attribute :description - dependency Factory::Resource::Group, as: :group - - product :group - product :name + attribute :group do + Factory::Resource::Group.fabricate! + end - product :repository_ssh_location do - Page::Project::Show.act do - choose_repository_clone_ssh - repository_location + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location end end - product :repository_http_location do - Page::Project::Show.act do - choose_repository_clone_http - repository_location + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location end end @@ -37,7 +36,7 @@ module QA def fabricate! group.visit! - Page::Group::Show.act { go_to_new_project } + Page::Group::Show.perform(&:go_to_new_project) Page::Project::New.perform do |page| page.choose_test_namespace diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb index a45e7fee03b..ce20641e6cc 100644 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ b/qa/qa/factory/resource/project_imported_from_github.rb @@ -4,16 +4,17 @@ module QA module Factory module Resource class ProjectImportedFromGithub < Resource::Project + attr_accessor :name attr_writer :personal_access_token, :github_repository_path - dependency Factory::Resource::Group, as: :group - - product :name + attribute :group do + Factory::Resource::Group.fabricate! + end def fabricate! group.visit! - Page::Group::Show.act { go_to_new_project } + Page::Group::Show.perform(&:go_to_new_project) Page::Project::New.perform do |page| page.go_to_import_project diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index 35383842142..383f534c12c 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -2,12 +2,12 @@ module QA module Factory module Resource class ProjectMilestone < Factory::Base - attr_accessor :description attr_reader :title + attr_accessor :description - dependency Factory::Resource::Project, as: :project - - product :title + attribute :project do + Factory::Resource::Project.fabricate! + end def title=(title) @title = "#{title}-#{SecureRandom.hex(4)}" @@ -17,12 +17,12 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act do - click_issues - click_milestones + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones end - Page::Project::Milestone::Index.act { click_new_milestone } + Page::Project::Milestone::Index.perform(&:click_new_milestone) Page::Project::Milestone::New.perform do |milestone_new| milestone_new.set_title(@title) diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb index 7ac65fe6913..7108db1e55a 100644 --- a/qa/qa/factory/resource/runner.rb +++ b/qa/qa/factory/resource/runner.rb @@ -6,9 +6,11 @@ module QA class Runner < Factory::Base attr_writer :name, :tags, :image - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-ci-cd' - project.description = 'Project with CI/CD Pipelines' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end end def name @@ -26,7 +28,7 @@ module QA def fabricate! project.visit! - Page::Project::Menu.act { click_ci_cd_settings } + Page::Project::Menu.perform(&:click_ci_cd_settings) Service::Runner.new(name).tap do |runner| Page::Project::Settings::CICD.perform do |settings| diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index e592f4e0dd2..a125bac65dd 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -8,17 +8,14 @@ module QA class Sandbox < Factory::Base attr_reader :path - product :id do - true # We don't retrieve the Group ID when using the Browser UI - end - product :path + attribute :id def initialize @path = Runtime::Namespace.sandbox_name end def fabricate! - Page::Main::Menu.act { go_to_groups } + Page::Main::Menu.perform(&:go_to_groups) Page::Dashboard::Groups.perform do |page| if page.has_group?(path) diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb deleted file mode 100644 index 4084a7fc2cd..00000000000 --- a/qa/qa/factory/resource/secret_variable.rb +++ /dev/null @@ -1,28 +0,0 @@ -module QA - module Factory - module Resource - class SecretVariable < Factory::Base - attr_accessor :key, :value - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-secret-variables' - project.description = 'project for adding secret variable test' - end - - def fabricate! - project.visit! - - Page::Project::Menu.act { click_ci_cd_settings } - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_secret_variables do |page| - page.fill_variable(key, value) - - page.save_variables - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index a512d071dd4..6f952eda36f 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -7,20 +7,16 @@ module QA extend Forwardable attr_accessor :title - attr_reader :private_key, :public_key, :fingerprint - def_delegators :key, :private_key, :public_key, :fingerprint - product :private_key - product :title - product :fingerprint + def_delegators :key, :private_key, :public_key, :fingerprint def key @key ||= Runtime::Key::RSA.new end def fabricate! - Page::Main::Menu.act { go_to_profile_settings } - Page::Profile::Menu.act { click_ssh_keys } + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |page| page.add_key(public_key, title) diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index 36edf787b64..e361face1f0 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -5,7 +5,7 @@ module QA module Resource class User < Factory::Base attr_reader :unique_id - attr_writer :username, :password, :name, :email + attr_writer :username, :password def initialize @unique_id = SecureRandom.hex(8) @@ -31,14 +31,9 @@ module QA defined?(@username) && defined?(@password) end - product :name - product :username - product :email - product :password - def fabricate! # Don't try to log-out if we're not logged-in - if Page::Main::Menu.act { has_personal_area?(wait: 0) } + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } Page::Main::Menu.perform { |main| main.sign_out } end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb index d697433736e..769f394e85c 100644 --- a/qa/qa/factory/resource/wiki.rb +++ b/qa/qa/factory/resource/wiki.rb @@ -4,9 +4,11 @@ module QA class Wiki < Factory::Base attr_accessor :title, :content, :message - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-wikis' - project.description = 'project for adding wikis' + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end end def fabricate! diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb index 5e8f883e25f..4e32382f910 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/factory/settings/hashed_storage.rb @@ -5,9 +5,9 @@ module QA def fabricate!(*traits) raise ArgumentError unless traits.include?(:enabled) - Page::Main::Login.act { sign_in_using_credentials } - Page::Main::Menu.act { go_to_admin_area } - Page::Admin::Menu.act { go_to_repository_settings } + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_repository_settings) Page::Admin::Settings::Repository.perform do |setting| setting.expand_repository_storage do |page| @@ -16,7 +16,7 @@ module QA end end - QA::Page::Main::Menu.act { sign_out } + QA::Page::Main::Menu.perform(&:sign_out) end end end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index c6a8891d398..27a88534258 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -113,21 +113,17 @@ module QA attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file - def debug? - Runtime::Env.respond_to?(:verbose?) && Runtime::Env.verbose? - end - def ssh_key_set? !private_key_file.nil? end def run(command_str) command = [env_vars, command_str, '2>&1'].compact.join(' ') - warn "DEBUG: command=[#{command}]" if debug? + Runtime::Logger.debug "Git: command=[#{command}]" output, _ = Open3.capture2(command) output = output.chomp.gsub(/\s+$/, '') - warn "DEBUG: output=[#{output}]" if debug? + Runtime::Logger.debug "Git: output=[#{output}]" output end 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/runtime/logger.rb b/qa/qa/runtime/logger.rb index 3baa24de0ec..bd5c4fe5bf5 100644 --- a/qa/qa/runtime/logger.rb +++ b/qa/qa/runtime/logger.rb @@ -7,14 +7,16 @@ module QA module Logger extend SingleForwardable - def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown + def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown singleton_class.module_eval do + attr_writer :logger + def logger return @logger if @logger @logger = ::Logger.new Runtime::Env.log_destination - @logger.level = ::Logger::DEBUG + @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR @logger end end 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 c98ede25b68..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' @@ -49,11 +49,13 @@ module QA cluster.install_prometheus = true cluster.install_runner = true end + kubernetes_cluster.populate(:ingress_ip) project.visit! Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Settings::CICD.perform do |p| - p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") + p.enable_auto_devops_with_domain( + "#{kubernetes_cluster.ingress_ip}.nip.io") end project.visit! diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index 229f93a1041..e9584a27d63 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -4,8 +4,7 @@ describe QA::Factory::Base do include Support::StubENV let(:factory) { spy('factory') } - let(:product) { spy('product') } - let(:product_location) { 'http://product_location' } + let(:location) { 'http://location' } shared_context 'fabrication context' do subject do @@ -17,9 +16,8 @@ describe QA::Factory::Base do end before do - allow(subject).to receive(:current_url).and_return(product_location) + allow(subject).to receive(:current_url).and_return(location) allow(subject).to receive(:new).and_return(factory) - allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product) end end @@ -28,7 +26,7 @@ describe QA::Factory::Base do it 'yields factory before calling factory method' do expect(factory).to receive(:something!).ordered - expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location) + expect(factory).to receive(fabrication_method_used).ordered.and_return(location) subject.public_send(fabrication_method_called, factory: factory) do |factory| factory.something! @@ -37,7 +35,7 @@ describe QA::Factory::Base do it 'does not log the factory and build method when QA_DEBUG=false' do stub_env('QA_DEBUG', 'false') - expect(factory).to receive(fabrication_method_used).and_return(product_location) + expect(factory).to receive(fabrication_method_used).and_return(location) expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } .not_to output.to_stdout @@ -71,17 +69,17 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_api! - it 'instantiates the factory, calls factory method returns fabrication product' do - expect(factory).to receive(:fabricate_via_api!).and_return(product_location) + it 'instantiates the factory, calls factory method returns the resource' do + expect(factory).to receive(:fabricate_via_api!).and_return(location) result = subject.fabricate_via_api!(factory: factory, parents: []) - expect(result).to eq(product) + expect(result).to eq(factory) end it 'logs the factory and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect(factory).to receive(:fabricate_via_api!).and_return(product_location) + expect(factory).to receive(:fabricate_via_api!).and_return(location) expect { subject.fabricate_via_api!(factory: factory, parents: []) } .to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/) @@ -100,10 +98,10 @@ describe QA::Factory::Base do expect(factory).to have_received(:fabricate!).with('something') end - it 'returns fabrication product' do + it 'returns fabrication resource' do result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) - expect(result).to eq(product) + expect(result).to eq(factory) end it 'logs the factory and build method when QA_DEBUG=true' do @@ -115,73 +113,134 @@ describe QA::Factory::Base do end end - describe '.dependency' do - let(:dependency) { spy('dependency') } + shared_context 'simple factory' do + subject do + Class.new(QA::Factory::Base) do + attribute :test do + 'block' + end - before do - stub_const('Some::MyDependency', dependency) - end + attribute :no_block - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep do |factory| - factory.something! + def fabricate! + 'any' + end + + def self.current_url + 'http://stub' end end end - it 'appends a new dependency and accessors' do - expect(subject.dependencies).to be_one + let(:factory) { subject.new } + end + + describe '.attribute' do + include_context 'simple factory' + + it 'appends new attribute' do + expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) end - it 'defines dependency accessors' do - expect(subject.new).to respond_to :mydep, :mydep= + context 'when the attribute is populated via a block' do + it 'returns value from the block' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(described_class) + expect(result.test).to eq('block') + end end - describe 'dependencies fabrication' do - let(:dependency) { double('dependency') } - let(:instance) { spy('instance') } + context 'when the attribute is populated via the api' do + let(:api_resource) { { no_block: 'api' } } + + before do + expect(factory).to receive(:api_resource).and_return(api_resource) + end + + it 'returns value from api' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(described_class) + expect(result.no_block).to eq('api') + end + + context 'when the attribute also has a block' do + let(:api_resource) { { test: 'api_with_block' } } + + before do + allow(QA::Runtime::Logger).to receive(:info) + end + + it 'returns value from api and emits an INFO log entry' do + result = subject.fabricate!(factory: factory) - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep + expect(result).to be_a(described_class) + expect(result.test).to eq('api_with_block') + expect(QA::Runtime::Logger) + .to have_received(:info).with(/api_with_block/) end end + end + context 'when the attribute is populated via direct assignment' do before do - stub_const('Some::MyDependency', dependency) + factory.test = 'value' + end + + it 'returns value from the assignment' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(described_class) + expect(result.test).to eq('value') + end - allow(subject).to receive(:new).and_return(instance) - allow(subject).to receive(:current_url).and_return(product_location) - allow(instance).to receive(:mydep).and_return(nil) - expect(QA::Factory::Product).to receive(:populate!) + context 'when the api also has such response' do + before do + allow(factory).to receive(:api_resource).and_return({ test: 'api' }) + end + + it 'returns value from the assignment' do + result = subject.fabricate!(factory: factory) + + expect(result).to be_a(described_class) + expect(result.test).to eq('value') + end end + end - it 'builds all dependencies first' do - expect(dependency).to receive(:fabricate!).once + context 'when the attribute has no value' do + it 'raises an error because no values could be found' do + result = subject.fabricate!(factory: factory) - subject.fabricate! + expect { result.no_block } + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.") end end end - describe '.product' do - include_context 'fabrication context' + describe '#web_url' do + include_context 'simple factory' - subject do - Class.new(described_class) do - def fabricate! - "any" - end + it 'sets #web_url to #current_url after fabrication' do + subject.fabricate!(factory: factory) - product :token - end + expect(factory.web_url).to eq(subject.current_url) end + end + + describe '#visit!' do + include_context 'simple factory' + + before do + allow(factory).to receive(:visit) + end + + it 'calls #visit with the underlying #web_url' do + factory.web_url = subject.current_url + factory.visit! - it 'appends new product attribute' do - expect(subject.attributes).to be_one - expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute) - expect(subject.attributes[0].name).to eq(:token) + expect(factory).to have_received(:visit).with(subject.current_url) end end end diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb deleted file mode 100644 index 657beddffb1..00000000000 --- a/qa/spec/factory/dependency_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -describe QA::Factory::Dependency do - let(:dependency) { spy('dependency' ) } - let(:factory) { spy('factory') } - let(:block) { spy('block') } - - let(:signature) do - double('signature', name: :mydep, factory: dependency, block: block) - end - - subject do - described_class.new(factory, signature) - end - - describe '#overridden?' do - it 'returns true if factory has overridden dependency' do - allow(factory).to receive(:mydep).and_return('something') - - expect(subject).to be_overridden - end - - it 'returns false if dependency has not been overridden' do - allow(factory).to receive(:mydep).and_return(nil) - - expect(subject).not_to be_overridden - end - end - - describe '#build!' do - context 'when dependency has been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(true) - end - - it 'does not fabricate dependency' do - subject.build! - - expect(dependency).not_to have_received(:fabricate!) - end - end - - context 'when dependency has not been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(false) - end - - it 'fabricates dependency' do - subject.build! - - expect(dependency).to have_received(:fabricate!) - end - - it 'sets product in the factory' do - subject.build! - - expect(factory).to have_received(:mydep=).with(dependency) - end - - it 'calls given block with dependency factory and caller factory' do - expect(dependency).to receive(:fabricate!).and_yield(dependency) - - subject.build! - - expect(block).to have_received(:call).with(dependency, factory) - end - - context 'with no block given' do - let(:signature) do - double('signature', name: :mydep, factory: dependency, block: nil) - end - - it 'does not error' do - subject.build! - - expect(dependency).to have_received(:fabricate!) - end - end - end - end -end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb deleted file mode 100644 index 43b1d93d769..00000000000 --- a/qa/spec/factory/product_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -describe QA::Factory::Product do - let(:factory) do - Class.new(QA::Factory::Base) do - def foo - 'bar' - end - end.new - end - - let(:product) { spy('product') } - let(:product_location) { 'http://product_location' } - - subject { described_class.new(factory, product_location) } - - describe '.populate!' do - before do - expect(factory.class).to receive(:attributes).and_return(attributes) - end - - context 'when the product attribute is populated via a block' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:test, proc { 'returned' })] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - result = described_class.populate!(factory, product_location) - - expect(result).to be_a(described_class) - expect(result.test).to eq('returned') - end - end - - context 'when the product attribute is populated via the api' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:test)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - expect(factory).to receive(:api_resource).and_return({ test: 'returned' }) - - result = described_class.populate!(factory, product_location) - - expect(result).to be_a(described_class) - expect(result.test).to eq('returned') - end - end - - context 'when the product attribute is populated via a factory attribute' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:foo)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - result = described_class.populate!(factory, product_location) - - expect(result).to be_a(described_class) - expect(result.foo).to eq('bar') - end - end - - context 'when the product attribute has no value' do - let(:attributes) do - [QA::Factory::Product::Attribute.new(:bar)] - end - - it 'returns a fabrication product and defines factory attributes as its methods' do - expect { described_class.populate!(factory, product_location) } - .to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.") - end - end - end - - describe '.visit!' do - it 'makes it possible to visit fabrication product' do - allow_any_instance_of(described_class) - .to receive(:visit).and_return('visited some url') - - expect(subject.visit!).to eq 'visited some url' - end - end -end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 9f17de4edbf..9d56353062b 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -3,9 +3,15 @@ require 'capybara/dsl' describe QA::Support::Page::Logging do + include Support::StubENV + let(:page) { double().as_null_object } before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + allow(Capybara).to receive(:current_session).and_return(page) allow(page).to receive(:current_url).and_return('http://current-url') allow(page).to receive(:has_css?).with(any_args).and_return(true) diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb index 794e1f9bfe6..44be3381bff 100644 --- a/qa/spec/runtime/logger_spec.rb +++ b/qa/spec/runtime/logger_spec.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true describe QA::Runtime::Logger do + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + described_class.logger = logger + end + it 'logs debug' do expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process end 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/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index c365988a100..98946e4287b 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -208,11 +208,22 @@ describe Boards::IssuesController do end end - context 'with unauthorized user' do - it 'returns a forbidden 403 response' do - create_issue user: guest, board: board, list: list1, title: 'New issue' + context 'with guest user' do + context 'in open list' do + it 'returns a successful 200 response' do + open_list = board.lists.create(list_type: :backlog) + create_issue user: guest, board: board, list: open_list, title: 'New issue' - expect(response).to have_gitlab_http_status(403) + expect(response).to have_gitlab_http_status(200) + end + end + + context 'in label list' do + it 'returns a forbidden 403 response' do + create_issue user: guest, board: board, list: list1, title: 'New issue' + + expect(response).to have_gitlab_http_status(403) + 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 bf41aa0706f..f7a4a4192d6 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Groups::BoardsController do let(:group) { create(:group) } - let(:user) { create(:user) } + let(:user) { create(:user) } before do group.add_maintainer(user) @@ -22,6 +22,27 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to redirect_to(group_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards format: :html + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true) @@ -35,12 +56,30 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, group: group) + create(:board_group_recent_visit, group: board.group, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'return an array with one group board' do create(:board, group: group) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -74,7 +113,7 @@ describe Groups::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -93,10 +132,25 @@ describe Groups::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:group) { create(:group, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') 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/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb index 096efc1c7b2..667eaa5e34f 100644 --- a/spec/controllers/projects/boards_controller_spec.rb +++ b/spec/controllers/projects/boards_controller_spec.rb @@ -28,6 +28,27 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end + it 'redirects to latest visited board' do + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to redirect_to(namespace_project_board_path(id: board.id)) + end + + it 'renders template if visited board is not found' do + visited = double + + allow(visited).to receive(:board_id).and_return(12) + allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + context 'with unauthorized user' do before do allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) @@ -41,12 +62,30 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'renders template' do + sign_out(user) + + board = create(:board, project: project) + create(:board_project_recent_visit, project: board.project, board: board, user: user) + + list_boards + + expect(response).to render_template :index + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns a list of project boards' do create_list(:board, 2, project: project) + expect(Boards::Visits::LatestService).not_to receive(:new) + list_boards format: :json parsed_response = JSON.parse(response.body) @@ -98,7 +137,7 @@ describe Projects::BoardsController do context 'when format is HTML' do it 'renders template' do - read_board board: board + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(1) expect(response).to render_template :show expect(response.content_type).to eq 'text/html' @@ -117,10 +156,25 @@ describe Projects::BoardsController do expect(response.content_type).to eq 'text/html' end end + + context 'when user is signed out' do + let(:project) { create(:project, :public) } + + it 'does not save visit' do + sign_out(user) + + expect { read_board board: board }.to change(BoardProjectRecentVisit, :count).by(0) + + expect(response).to render_template :show + expect(response.content_type).to eq 'text/html' + end + end end context 'when format is JSON' do it 'returns project board' do + expect(Boards::Visits::CreateService).not_to receive(:new) + read_board board: board, format: :json expect(response).to match_response_schema('board') diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 2023d4b0bd0..8eb01145ed5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -152,11 +152,33 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z}) expect(json_response['new_issue_path']).to include('/issues/new') end end + context 'when job is running' do + context 'job is cancelable' do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + + it 'cancel_path is present with correct redirect' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path'])) + end + end + + context 'with web terminal' do + let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } + + it 'exposes the terminal path' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['terminal_path']).to match(%r{/terminal}) + end + end + end + context 'when job has artifacts' do context 'with not expiry date' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } @@ -185,16 +207,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - context 'when job has terminal' do - let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - - it 'exposes the terminal path' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['terminal_path']).to match(%r{/terminal}) - end - end - context 'when job passed with no trace' do let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } 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/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb index 6114eef7003..00c1e617e3a 100644 --- a/spec/controllers/projects/mirrors_controller_spec.rb +++ b/spec/controllers/projects/mirrors_controller_spec.rb @@ -63,6 +63,69 @@ describe Projects::MirrorsController do end end + describe '#ssh_host_keys', :use_clean_rails_memory_store_caching do + let(:project) { create(:project) } + let(:cache) { SshHostKey.new(project: project, url: "ssh://example.com:22") } + + before do + sign_in(project.owner) + end + + context 'invalid URLs' do + %w[ + INVALID + git@example.com:foo/bar.git + ssh://git@example.com:foo/bar.git + ].each do |url| + it "returns an error with a 400 response for URL #{url.inspect}" do + do_get(project, url) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'Invalid URL') + end + end + end + + context 'no data in cache' do + it 'requests the cache to be filled and returns a 204 response' do + expect(ReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once) + + do_get(project) + + expect(response).to have_gitlab_http_status(204) + end + end + + context 'error in the cache' do + it 'returns the error with a 400 response' do + stub_reactive_cache(cache, error: 'An error') + + do_get(project) + + expect(response).to have_gitlab_http_status(400) + expect(json_response).to eq('message' => 'An error') + end + end + + context 'data in the cache' do + let(:ssh_key) { 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf' } + let(:ssh_fp) { { type: 'ed25519', bits: 256, fingerprint: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16', index: 0 } } + + it 'returns the data with a 200 response' do + stub_reactive_cache(cache, known_hosts: ssh_key) + + do_get(project) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq('known_hosts' => ssh_key, 'fingerprints' => [ssh_fp.stringify_keys], 'host_keys_changed' => true) + end + end + + def do_get(project, url = 'ssh://example.com') + get :ssh_host_keys, namespace_id: project.namespace, project_id: project, ssh_url: url + end + end + def do_put(project, options, extra_attrs = {}) attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param) attrs[:project] = options 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/board_group_recent_visit.rb b/spec/factories/board_group_recent_visit.rb new file mode 100644 index 00000000000..97ad5d6d068 --- /dev/null +++ b/spec/factories/board_group_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_group_recent_visit do + user + group + board + end +end diff --git a/spec/factories/board_project_recent_visit.rb b/spec/factories/board_project_recent_visit.rb new file mode 100644 index 00000000000..49ae4d7b391 --- /dev/null +++ b/spec/factories/board_project_recent_visit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :board_project_recent_visit do + user + project + board + end +end 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/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 134eb25e4b1..fe475e1f7a0 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -8,3 +8,4 @@ require_relative 'support/rspec' require 'active_support/all' ActiveSupport::Dependencies.autoload_paths << 'lib' +ActiveSupport::Dependencies.autoload_paths << 'ee/lib' diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 96dfde2e08c..735ca60f7da 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -53,13 +53,33 @@ describe 'Admin Groups' do expect_selected_visibility(internal) end - it 'when entered in group path, it auto filled the group name', :js do + it 'when entered in group name, it auto filled the group path', :js do visit admin_groups_path click_link "New group" - group_path = 'gitlab' + group_name = 'gitlab' + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq group_name + end + + it 'auto populates the group path with the group name', :js do + visit admin_groups_path + click_link "New group" + group_name = 'my gitlab project' + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq 'my-gitlab-project' + end + + it 'when entering in group path, group name does not change anymore', :js do + visit admin_groups_path + click_link "New group" + group_path = 'my-gitlab-project' + group_name = 'My modified gitlab project' fill_in 'group_path', with: group_path - name_field = find('input#group_name') - expect(name_field.value).to eq group_path + fill_in 'group_name', with: group_name + path_field = find('input#group_path') + expect(path_field.value).to eq 'my-gitlab-project' end end 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/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0bf1ecbc433..164442a47f5 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -94,8 +94,14 @@ describe 'Issue Boards new issue', :js do wait_for_requests end - it 'does not display new issue button' do - expect(page).to have_selector('.issue-count-badge-add-button', count: 0) + it 'displays new issue button in open list' do + expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + end + + it 'does not display new issue button in label list' do + page.within('.board:nth-child(2)') do + expect(page).not_to have_selector('.issue-count-badge-add-button') + end end end end 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/group_spec.rb b/spec/features/dashboard/group_spec.rb index e57fcde8b2c..259f220c68b 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -14,15 +14,15 @@ RSpec.describe 'Dashboard Group' do it 'creates new group', :js do visit dashboard_groups_path find('.btn-success').click - new_path = 'Samurai' + new_name = 'Samurai' new_description = 'Tokugawa Shogunate' - fill_in 'group_path', with: new_path + fill_in 'group_name', with: new_name fill_in 'group_description', with: new_description click_button 'Create group' - expect(current_path).to eq group_path(Group.find_by(name: new_path)) - expect(page).to have_content(new_path) + expect(current_path).to eq group_path(Group.find_by(name: new_name)) + expect(page).to have_content(new_name) expect(page).to have_content(new_description) end end 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/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb index 11f05b6d220..259f22139ef 100644 --- a/spec/features/explore/new_menu_spec.rb +++ b/spec/features/explore/new_menu_spec.rb @@ -29,7 +29,7 @@ describe 'Top Plus Menu', :js do click_topmenuitem("New group") - expect(page).to have_content('Group path') + expect(page).to have_content('Group URL') expect(page).to have_content('Group name') end @@ -79,7 +79,7 @@ describe 'Top Plus Menu', :js do click_topmenuitem("New subgroup") - expect(page).to have_content('Group path') + expect(page).to have_content('Group URL') expect(page).to have_content('Group name') 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/groups_spec.rb b/spec/features/groups_spec.rb index 63aa26cf5fd..4d04b8043ec 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -7,7 +7,7 @@ describe 'Group' do matcher :have_namespace_error_message do match do |page| - page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.") + page.has_content?("Group URL can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.") end end @@ -18,7 +18,7 @@ describe 'Group' do describe 'with space in group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'space group' + fill_in 'Group URL', with: 'space group' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -28,7 +28,7 @@ describe 'Group' do describe 'with .atom at end of group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'atom_group.atom' + fill_in 'Group URL', with: 'atom_group.atom' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -38,7 +38,7 @@ describe 'Group' do describe 'with .git at end of group path' do it 'renders new group form with validation errors' do - fill_in 'Group path', with: 'git_group.git' + fill_in 'Group URL', with: 'git_group.git' click_button 'Create group' expect(current_path).to eq(groups_path) @@ -94,7 +94,8 @@ describe 'Group' do end it 'creates a nested group' do - fill_in 'Group path', with: 'bar' + fill_in 'Group name', with: 'bar' + fill_in 'Group URL', with: 'bar' click_button 'Create group' expect(current_path).to eq(group_path('foo/bar')) @@ -112,7 +113,8 @@ describe 'Group' do visit new_group_path(group, parent_id: group.id) - fill_in 'Group path', with: 'bar' + fill_in 'Group name', with: 'bar' + fill_in 'Group URL', with: 'bar' click_button 'Create group' expect(current_path).to eq(group_path('foo/bar')) 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/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb index 40c452c991a..b0764db7751 100644 --- a/spec/features/issues/resource_label_events_spec.rb +++ b/spec/features/issues/resource_label_events_spec.rb @@ -7,6 +7,7 @@ describe 'List issue resource label events', :js do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, author: user) } let!(:label) { create(:label, project: project, title: 'foo') } + let!(:user_status) { create(:user_status, user: user) } context 'when user displays the issue' do let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') } @@ -23,6 +24,12 @@ describe 'List issue resource label events', :js do expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label' end end + + it 'shows the user status on the system note for the label' do + page.within("#note_#{event.discussion_id}") do + expect(page).to show_user_status user_status + end + end end context 'when user adds label to the issue' do 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..0c610edd6d1 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 @@ -174,7 +179,7 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again') + expect(page).to have_text(%r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) end end diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index 45cccbee63e..41f447fba95 100644 --- a/spec/features/merge_request/user_sees_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -41,8 +41,8 @@ describe 'Merge request > User sees pipelines', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page.find('.ci-widget')).to have_content( - 'Could not connect to the CI server. Please check your settings and try again') + expect(page.find('.ci-widget')).to have_text( + %r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) end 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/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index b3bea92e635..5cb3f7c732f 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -198,6 +198,24 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end + context 'when job is running', :js do + let(:job) { create(:ci_build, :running, pipeline: pipeline) } + let(:job_url) { project_job_path(project, job) } + + before do + visit job_url + wait_for_requests + end + + context 'job is cancelable' do + it 'shows cancel button' do + click_link 'Cancel' + + expect(page.current_path).to eq(job_url) + end + end + end + context "Job from other project" do before do visit project_job_path(project, job2) 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/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 3ee753b7d23..7225ca65492 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe 'User searches for wiki pages', :js do let(:user) { create(:user) } - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) } before do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 0689c843104..2f164ffa8b0 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -57,11 +57,37 @@ describe IssuesFinder do end context 'filtering by no assignee' do - let(:params) { { assignee_id: 0 } } + let(:params) { { assignee_id: 'None' } } - it 'returns issues not assign to any assignee' do + it 'returns issues not assigned to any assignee' do expect(issues).to contain_exactly(issue4) end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 0 + + expect(issues).to contain_exactly(issue4) + end + + it 'returns issues not assigned to any assignee' do + params[:assignee_id] = 'none' + + expect(issues).to contain_exactly(issue4) + end + end + + context 'filtering by any assignee' do + let(:params) { { assignee_id: 'Any' } } + + it 'returns issues assigned to any assignee' do + expect(issues).to contain_exactly(issue1, issue2, issue3) + end + + it 'returns issues assigned to any assignee' do + params[:assignee_id] = 'any' + + expect(issues).to contain_exactly(issue1, issue2, issue3) + end end context 'filtering by group_id' do 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/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 1c216b3fe97..f709f152c92 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -3,63 +3,13 @@ require 'spec_helper' describe BlobHelper do include TreeHelper - let(:blob_name) { 'test.lisp' } - let(:no_context_content) { ":type \"assem\"))" } - let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } - let(:split_content) { blob_content.split("\n") } - let(:multiline_content) do - %q( - def test(input): - """This is line 1 of a multi-line comment. - This is line 2. - """ - ) - end - describe '#highlight' do - it 'returns plaintext for unknown lexer context' do - result = helper.highlight(blob_name, no_context_content) - expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>]) + it 'wraps highlighted content' do + expect(helper.highlight('test.rb', '52')).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="ruby"><span class="mi">52</span></span></code></pre>]) end - it 'returns plaintext for long blobs' do - stub_const('Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1) - result = helper.highlight(blob_name, blob_content) - - expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span></code></pre>]) - end - - it 'highlights single block' do - expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> -<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] - - expect(helper.highlight(blob_name, blob_content)).to eq(expected) - end - - it 'highlights multi-line comments' do - result = helper.highlight(blob_name, multiline_content) - html = Nokogiri::HTML(result) - lines = html.search('.s') - expect(lines.count).to eq(3) - expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') - expect(lines[1].text).to eq(' This is line 2.') - expect(lines[2].text).to eq(' """') - end - - context 'diff highlighting' do - let(:blob_name) { 'test.diff' } - let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} - let(:expected) do - %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> -<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> -<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span> -<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>) - end - - it 'highlights each line properly' do - result = helper.highlight(blob_name, blob_content) - expect(result).to eq(expected) - end + it 'handles plain version' do + expect(helper.highlight('test.rb', '52', plain: true)).to eq(%q[<pre class="code highlight"><code><span id="LC1" class="line" lang="">52</span></code></pre>]) end end 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/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index 460a2e1b5da..424092d2d88 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -140,10 +140,11 @@ describe('Sidebar details block', () => { }); describe('while fetching stages', () => { - it('renders dropdown with More label', () => { + it('it does not render dropdown', () => { + store.dispatch('requestStages'); vm = mountComponentWithStore(SidebarComponent, { store }); - expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual('More'); + expect(vm.$el.querySelector('.js-selected-stage')).toBeNull(); }); }); diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/javascripts/jobs/store/mutations_spec.js index 4230a7c42cf..d7908efcf13 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/javascripts/jobs/store/mutations_spec.js @@ -124,8 +124,8 @@ describe('Jobs Store Mutations', () => { expect(stateCopy.job).toEqual({ id: 1312321 }); }); - it('sets selectedStage when the selectedStage is More', () => { - expect(stateCopy.selectedStage).toEqual('More'); + it('sets selectedStage when the selectedStage is empty', () => { + expect(stateCopy.selectedStage).toEqual(''); mutations[types.RECEIVE_JOB_SUCCESS](stateCopy, { id: 1312321, stage: 'deploy' }); expect(stateCopy.selectedStage).toEqual('deploy'); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index 2821f4d6793..de6b96aab57 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -352,3 +352,21 @@ describe('prettyTime methods', () => { }); }); }); + +describe('calculateRemainingMilliseconds', () => { + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); + }); + + it('calculates the remaining time for a given end date', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-04T01:44:03Z'); + + expect(milliseconds).toBe(3723000); + }); + + it('returns 0 if the end date has passed', () => { + const milliseconds = datetimeUtility.calculateRemainingMilliseconds('2063-04-03T00:00:00Z'); + + expect(milliseconds).toBe(0); + }); +}); diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js index bb7a29fe30a..b9e805628f8 100644 --- a/spec/javascripts/lib/utils/text_markdown_spec.js +++ b/spec/javascripts/lib/utils/text_markdown_spec.js @@ -166,6 +166,33 @@ describe('init markdown', () => { expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select)); expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length); }); + + it('should support selected urls', () => { + const expectedUrl = 'http://www.gitlab.com'; + const expectedSelectionText = 'text'; + const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`; + const initialValue = `text ${expectedUrl} text`; + + textArea.value = initialValue; + const selectedIndex = initialValue.indexOf(expectedUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: expectedUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, + ); + }); }); }); }); 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/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index b5c62178642..a7dcd532f4f 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -62,9 +62,13 @@ describe('Pipelines Actions dropdown', () => { ); }; - beforeEach(() => { + beforeEach(done => { spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime()); vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] }); + + Vue.nextTick() + .then(done) + .catch(done.fail); }); it('emits postAction event after confirming', () => { diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index ce850bc621e..3d44af11153 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,64 @@ describe('Deployment component', () => { expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); }); }); + + describe('without changes', () => { + beforeEach(() => { + window.gon = window.gon || {}; + window.gon.features = window.gon.features || {}; + window.gon.features.ciEnvironmentsStatusChanges = true; + delete deploymentMockData.changes; + + vm = mountComponent(Component, { deployment: { ...deploymentMockData } }); + }); + + afterEach(() => { + delete window.gon.features.ciEnvironmentsStatusChanges; + }); + + it('renders the link to the review app without dropdown', () => { + expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull(); + 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/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index ea8007d2029..689580e6b91 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -22,6 +22,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, ciStatus: 'success', hasCi: true, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(true); @@ -30,6 +31,7 @@ describe('MRWidgetPipeline', () => { it('should return false when there is no pipeline', () => { vm = mountComponent(Component, { pipeline: {}, + troubleshootingDocsPath: 'help', }); expect(vm.hasPipeline).toEqual(false); @@ -42,6 +44,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(false); @@ -52,6 +55,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); expect(vm.hasCIError).toEqual(true); @@ -65,11 +69,12 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: null, + troubleshootingDocsPath: 'help', }); expect( vm.$el.querySelector('.media-body').textContent.trim(), - ).toEqual('Could not connect to the CI server. Please check your settings and try again'); + ).toContain('Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>'); }); describe('with a pipeline', () => { @@ -78,6 +83,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockData.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); @@ -122,6 +128,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); }); @@ -162,6 +169,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect( @@ -179,6 +187,7 @@ describe('MRWidgetPipeline', () => { pipeline: mockCopy.pipeline, hasCi: true, ciStatus: 'success', + troubleshootingDocsPath: 'help', }); expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); diff --git a/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js new file mode 100644 index 00000000000..68a65bd21c6 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/review_app_link_spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import component from '~/vue_merge_request_widget/components/review_app_link.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('review app link', () => { + const Component = Vue.extend(component); + const props = { + link: '/review', + cssClass: 'js-link', + }; + let vm; + let el; + + beforeEach(() => { + vm = mountComponent(Component, props); + el = vm.$el; + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided link as href attribute', () => { + expect(el.getAttribute('href')).toEqual(props.link); + }); + + it('renders provided cssClass as class attribute', () => { + expect(el.getAttribute('class')).toEqual(props.cssClass); + }); + + it('renders View app text', () => { + expect(el.textContent.trim()).toEqual('View app'); + }); + + it('renders svg icon', () => { + expect(el.querySelector('svg')).not.toBeNull(); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 7fd1a2350f7..c5e30a730cb 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -219,4 +219,5 @@ export default { only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', + troubleshooting_docs_path: 'help' }; 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/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js new file mode 100644 index 00000000000..929ffe219f4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/gl_countdown_spec.js @@ -0,0 +1,77 @@ +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import Vue from 'vue'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; + +describe('GlCountdown', () => { + const Component = Vue.extend(GlCountdown); + let vm; + let now = '2000-01-01T00:00:00Z'; + + beforeEach(() => { + spyOn(Date, 'now').and.callFake(() => new Date(now).getTime()); + jasmine.clock().install(); + }); + + afterEach(() => { + vm.$destroy(); + jasmine.clock().uninstall(); + }); + + describe('when there is time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '2000-01-01T01:02:03Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays remaining time', () => { + expect(vm.$el).toContainText('01:02:03'); + }); + + it('updates remaining time', done => { + now = '2000-01-01T00:00:01Z'; + jasmine.clock().tick(1000); + + Vue.nextTick() + .then(() => { + expect(vm.$el).toContainText('01:02:02'); + done(); + }) + .catch(done.fail); + }); + }); + + describe('when there is no time remaining', () => { + beforeEach(done => { + vm = mountComponent(Component, { + endDateString: '1900-01-01T00:00:00Z', + }); + + Vue.nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays 00:00:00', () => { + expect(vm.$el).toContainText('00:00:00'); + }); + }); + + describe('when an invalid date is passed', () => { + it('throws a validation error', () => { + spyOn(Vue.config, 'warnHandler').and.stub(); + vm = mountComponent(Component, { + endDateString: 'this is invalid', + }); + + expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1); + const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0); + + expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/); + }); + }); +}); 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/javascripts/vue_shared/directives/tooltip_spec.js b/spec/javascripts/vue_shared/directives/tooltip_spec.js index 305d2fd5af4..1d516a280b0 100644 --- a/spec/javascripts/vue_shared/directives/tooltip_spec.js +++ b/spec/javascripts/vue_shared/directives/tooltip_spec.js @@ -13,24 +13,45 @@ describe('Tooltip directive', () => { describe('with a single tooltip', () => { beforeEach(() => { - const SomeComponent = Vue.extend({ + setFixtures('<div id="dummy-element"></div>'); + vm = new Vue({ + el: '#dummy-element', directives: { tooltip, }, - template: ` - <div - v-tooltip - title="foo"> - </div> - `, + data() { + return { + tooltip: 'some text', + }; + }, + template: '<div v-tooltip :title="tooltip"></div>', }); - - vm = new SomeComponent().$mount(); }); it('should have tooltip plugin applied', () => { expect($(vm.$el).data('bs.tooltip')).toBeDefined(); }); + + it('displays the title as tooltip', () => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + expect(tooltipElement.innerText).toContain('some text'); + }); + + it('updates a visible tooltip', done => { + $(vm.$el).tooltip('show'); + const tooltipElement = document.querySelector('.tooltip-inner'); + + vm.tooltip = 'other text'; + + Vue.nextTick() + .then(() => { + expect(tooltipElement).toContainText('other text'); + done(); + }) + .catch(done.fail); + }); }); describe('with multiple tooltips', () => { diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb new file mode 100644 index 00000000000..41e6fb47b11 --- /dev/null +++ b/spec/lib/api/helpers/custom_validators_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe API::Helpers::CustomValidators do + let(:scope) do + Struct.new(:opts) do + def full_name(attr_name) + attr_name + end + end + end + + describe API::Helpers::CustomValidators::Absence do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'empty param' do + it 'does not raise a validation error' do + expect_no_validation_error({}) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_value' }) + end + end + end + + describe API::Helpers::CustomValidators::IntegerNoneAny do + subject do + described_class.new(['test'], {}, false, scope.new) + end + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => 2 }) + expect_no_validation_error({ 'test' => 100 }) + expect_no_validation_error({ 'test' => 'None' }) + expect_no_validation_error({ 'test' => 'Any' }) + expect_no_validation_error({ 'test' => 'none' }) + expect_no_validation_error({ 'test' => 'any' }) + end + end + + context 'invalid parameters' do + it 'should raise a validation error' do + expect_validation_error({ 'test' => 'some_other_string' }) + end + end + end + + def expect_no_validation_error(params) + expect { validate_test_param!(params) }.not_to raise_error + end + + def expect_validation_error(params) + expect { validate_test_param!(params) }.to raise_error(Grape::Exceptions::Validation) + end + + def validate_test_param!(params) + subject.validate_param!('test', params) + end +end 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/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb index 18ebfef38f0..f431d4e2a53 100644 --- a/spec/lib/gitlab/git/attributes_parser_spec.rb +++ b/spec/lib/gitlab/git/attributes_parser_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do # It's a bit hard to test for something _not_ being processed. As such we'll # just test the number of entries. it 'ignores any comments and empty lines' do - expect(subject.patterns.length).to eq(10) + expect(subject.patterns.length).to eq(12) end end @@ -126,7 +126,7 @@ describe Gitlab::Git::AttributesParser, :seed_helper do describe '#each_line' do it 'iterates over every line in the attributes file' do - args = [String] * 14 # the number of lines in the file + args = [String] * 16 # the number of lines in the file expect { |b| subject.each_line(&b) }.to yield_successive_args(*args) end 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/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 51eb997a325..9a443fa7f20 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1194,6 +1194,34 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#gitattribute' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') } + + after do + ensure_seeds + end + + it 'returns matching language attribute' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby') + end + + it 'returns matching language attribute with additional options' do + expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json') + end + + it 'returns nil if nothing matches' do + expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil) + end + + context 'without gitattributes file' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + + it 'returns nil' do + expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil) + end + end + end + describe '#ref_exists?' do it 'returns true for an existing tag' do expect(repository.ref_exists?('refs/heads/master')).to eq(true) diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb new file mode 100644 index 00000000000..bcf4814edb6 --- /dev/null +++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::WrapsGitalyErrors do + subject(:wrapper) do + klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors } + klazz.new + end + + describe "#wrapped_gitaly_errors" do + mapping = { + GRPC::NotFound => Gitlab::Git::Repository::NoRepository, + GRPC::InvalidArgument => ArgumentError, + GRPC::BadStatus => Gitlab::Git::CommandError + } + + mapping.each do |grpc_error, error| + it "wraps #{grpc_error} in a #{error}" do + expect { wrapper.wrapped_gitaly_errors { raise grpc_error.new('wrapped') } } + .to raise_error(error) + end + end + + it 'does not swallow other errors' do + expect { wrapper.wrapped_gitaly_errors { raise 'raised' } } + .to raise_error(RuntimeError) + 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/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 88f7099ff3c..fe0e9702f8a 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -5,44 +5,85 @@ describe Gitlab::Highlight do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:commit) { project.commit(sample_commit.id) } - - describe 'custom highlighting from .gitattributes' do - let(:branch) { 'gitattributes' } - let(:blob) { repository.blob_at_branch(branch, path) } + describe 'language provided' do let(:highlighter) do - described_class.new(blob.path, blob.data, repository: repository) + described_class.new('foo.erb', 'bar', language: 'erb?parent=json') end - before do - project.change_head('gitattributes') + it 'sets correct lexer' do + expect(highlighter.lexer.tag).to eq 'erb' + expect(highlighter.lexer.parent.tag).to eq 'json' end + end - describe 'basic language selection' do - let(:path) { 'custom-highlighting/test.gitlab-custom' } - it 'highlights as ruby' do - expect(highlighter.lexer.tag).to eq 'ruby' - end + describe '#highlight' do + let(:file_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end + + it 'highlights' do + expected = %Q[<span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> +<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>] + + expect(described_class.highlight(file_name, content)).to eq(expected) + end + + it 'returns plain version for unknown lexer context' do + result = described_class.highlight(file_name, no_context_content) + + expect(result).to eq(%[<span id="LC1" class="line" lang="">:type "assem"))</span>]) end - describe 'cgi options' do - let(:path) { 'custom-highlighting/test.gitlab-cgi' } + it 'returns plain version for long content' do + stub_const('Gitlab::Highlight::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1) + result = described_class.highlight(file_name, content) - it 'highlights as json with erb' do - expect(highlighter.lexer.tag).to eq 'erb' - expect(highlighter.lexer.parent.tag).to eq 'json' + expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span>]) + end + + it 'highlights multi-line comments' do + result = described_class.highlight(file_name, multiline_content) + html = Nokogiri::HTML(result) + lines = html.search('.s') + + expect(lines.count).to eq(3) + expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') + end + + context 'diff highlighting' do + let(:file_name) { 'test.diff' } + let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"} + let(:expected) do + %q(<span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span> +<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span> +<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span> +<span id="LC4" class="line" lang="diff"> ddd</span>) + end + + it 'highlights each line properly' do + result = described_class.highlight(file_name, content) + + expect(result).to eq(expected) end end - end - describe '#highlight' do describe 'with CRLF' do let(:branch) { 'crlf-diff' } let(:path) { 'files/whitespace' } let(:blob) { repository.blob_at_branch(branch, path) } let(:lines) do - described_class.highlight(blob.path, blob.data, repository: repository).lines + described_class.highlight(blob.path, blob.data).lines end it 'strips extra LFs' do 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/patch/draw_route_spec.rb b/spec/lib/gitlab/patch/draw_route_spec.rb new file mode 100644 index 00000000000..4009b903dc3 --- /dev/null +++ b/spec/lib/gitlab/patch/draw_route_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Patch::DrawRoute do + subject do + Class.new do + include Gitlab::Patch::DrawRoute + + def route_path(route_name) + File.expand_path("../../../../#{route_name}", __dir__) + end + end.new + end + + before do + allow(subject).to receive(:instance_eval) + end + + it 'evaluates CE only route' do + subject.draw(:help) + + expect(subject).to have_received(:instance_eval) + .with(File.read(subject.route_path('config/routes/help.rb'))) + .once + + expect(subject).to have_received(:instance_eval) + .once + end +end 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/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb new file mode 100644 index 00000000000..59ad4e5417e --- /dev/null +++ b/spec/models/board_group_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardGroupRecentVisit do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:board) { create(:board, group: group) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:group) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, group: group) + board3 = create(:board, group: group) + + create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago + create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, group)).to eq recent + end + end +end diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb new file mode 100644 index 00000000000..275581945fa --- /dev/null +++ b/spec/models/board_project_recent_visit_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardProjectRecentVisit do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:board) { create(:board, project: project) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:board) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:board) } + end + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } + + it 'updates the timestamp' do + Timecop.freeze do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + it 'returns the most recent visited' do + board2 = create(:board, project: project) + board3 = create(:board, project: project) + + create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago + create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago + recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago + + expect(described_class.latest(user, project)).to eq recent + 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/blob_language_from_git_attributes_spec.rb b/spec/models/concerns/blob_language_from_git_attributes_spec.rb new file mode 100644 index 00000000000..7f05073b08e --- /dev/null +++ b/spec/models/concerns/blob_language_from_git_attributes_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BlobLanguageFromGitAttributes do + include FakeBlobHelpers + + let(:project) { build(:project, :repository) } + + describe '#language_from_gitattributes' do + subject(:blob) { fake_blob(path: 'file.md') } + + it 'returns return value from gitattribute' do + expect(blob.project.repository).to receive(:gitattribute).with(blob.path, 'gitlab-language').and_return('erb?parent=json') + + expect(blob.language_from_gitattributes).to eq('erb?parent=json') + end + + it 'returns nil if project is absent' do + allow(blob).to receive(:project).and_return(nil) + + expect(blob.language_from_gitattributes).to eq(nil) + end + end +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/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index 6e35511e848..911f85d7b28 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -2,12 +2,6 @@ require 'spec_helper' describe LfsObject do describe '#local_store?' do - it 'returns true when file_store is nil' do - subject.file_store = nil - - expect(subject.local_store?).to eq true - end - it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do subject.file_store = LfsObjectUploader::Store::LOCAL @@ -83,19 +77,6 @@ describe LfsObject do describe 'file is being stored' do let(:lfs_object) { create(:lfs_object, :with_file) } - context 'when object has nil store' do - before do - lfs_object.update_column(:file_store, nil) - lfs_object.reload - end - - it 'is stored locally' do - expect(lfs_object.file_store).to be(nil) - expect(lfs_object.file).to be_file_storage - expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) - end - end - context 'when existing object has local store' do it 'is stored locally' do expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL) 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/ssh_host_key_spec.rb b/spec/models/ssh_host_key_spec.rb new file mode 100644 index 00000000000..75db43b3d56 --- /dev/null +++ b/spec/models/ssh_host_key_spec.rb @@ -0,0 +1,164 @@ +require 'spec_helper' + +describe SshHostKey do + using RSpec::Parameterized::TableSyntax + include ReactiveCachingHelpers + + let(:key1) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3UpyF2iLqy1d63M6k3jH1vuEnq/NWtE+o' \ + 'rJe1Xn7JoRbduKd6zpsJ0JhBGWgcQK0ph0aGW5PcudzzBSc+SlYfCc4GTaxDtmj41hW0o72m' \ + 'NiuDW3oKXXShOiVRde2ZOquH8Z865jGiZIC8BI/bXZD29IGUih0hPu7Rjp70VYiE+35QRf/p' \ + 'sD0Ddrz8QUIG3A/2dMzLI5F5ZORk3BIX2F3mJwJOvZxRhR/SqyphDMZ5eZ0EzqbFBCDE6HAB' \ + 'Woz9ck8RBGLvCIggmDHj3FmMLcQGMDiy6wKp7QdnBtxjCP6vtE6YPUM223AqsWt+9NTtCfB8' \ + 'YdNAH7YcHHOR1FgtSk1x' + end + + let(:key2) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLIp+4ciR2YO9f9rpldc7InNQw/TBUtcNb' \ + 'J2XR0rr15/5ytz7YM16xXG0Qjx576PNSmqs4gbTrvTuFZak+v1Jx/9deHRq/yqp9f+tv33+i' \ + 'aJGCQCX/+OVY7aWgV2R9YsS7XQ4mnv4XlOTEssib/rGAIT+ATd/GcdYSEOO+dh4O09/6O/jI' \ + 'MGSeP+NNetgn1nPCnLOjrXFZUnUtNDi6EEKeIlrliJjSb7Jr4f7gjvZnv4RskWHHFo8FgAAq' \ + 't0gOMT6EmKrnypBe2vLGSAXbtkXr01q6/DNPH+n9VA1LTV6v1KN/W5CN5tQV11wRSKiM8g5O' \ + 'Ebi86VjJRi2sOuYoXQU1' + end + + # Purposefully ordered so that `sort` will make changes + let(:known_hosts) do + <<~EOF + example.com #{key1} git@localhost + @revoked other.example.com #{key2} git@localhost + EOF + end + + let(:extra) { known_hosts + "foo\nbar\n" } + let(:reversed) { known_hosts.lines.reverse.join } + + let(:compare_host_keys) { nil } + + def stub_ssh_keyscan(args, status: true, stdout: "", stderr: "") + stdin = StringIO.new + stdout = double(:stdout, read: stdout) + stderr = double(:stderr, read: stderr) + wait_thr = double(:wait_thr, value: double(success?: status)) + + expect(Open3).to receive(:popen3).with({}, 'ssh-keyscan', *args).and_yield(stdin, stdout, stderr, wait_thr) + + stdin + end + + let(:project) { build(:project) } + + subject(:ssh_host_key) { described_class.new(project: project, url: 'ssh://example.com:2222', compare_host_keys: compare_host_keys) } + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expected = [key1, key2] + .map { |data| Gitlab::SSHPublicKey.new(data) } + .each_with_index + .map { |key, i| { bits: key.bits, fingerprint: key.fingerprint, type: key.type, index: i } } + + expect(ssh_host_key.fingerprints.as_json).to eq(expected) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#fingerprints', :use_clean_rails_memory_store_caching do + it 'returns an array of indexed fingerprints when the cache is filled' do + stub_reactive_cache(ssh_host_key, known_hosts: known_hosts) + + expect(ssh_host_key.fingerprints.as_json).to eq( + [ + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key1).fingerprint, type: :rsa, index: 0 }, + { bits: 2048, fingerprint: Gitlab::SSHPublicKey.new(key2).fingerprint, type: :rsa, index: 1 } + ] + ) + end + + it 'returns an empty array when the cache is empty' do + expect(ssh_host_key.fingerprints).to eq([]) + end + end + + describe '#host_keys_changed?' do + where(:known_hosts_a, :known_hosts_b, :result) do + known_hosts | extra | true + known_hosts | "foo\n" | true + known_hosts | '' | true + known_hosts | nil | true + known_hosts | known_hosts | false + reversed | known_hosts | false + extra | "foo\n" | true + '' | '' | false + nil | nil | false + '' | nil | false + end + + with_them do + let(:compare_host_keys) { known_hosts_b } + + subject { ssh_host_key.host_keys_changed? } + + context '(normal)' do + let(:compare_host_keys) { known_hosts_b } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_a) + end + + it { is_expected.to eq(result) } + end + + # Comparisons should be symmetrical, so test the reverse too + context '(reversed)' do + let(:compare_host_keys) { known_hosts_a } + + before do + expect(ssh_host_key).to receive(:known_hosts).and_return(known_hosts_b) + end + + it { is_expected.to eq(result) } + end + end + end + + describe '#calculate_reactive_cache' do + subject(:cache) { ssh_host_key.calculate_reactive_cache } + + it 'writes the hostname to STDIN' do + stdin = stub_ssh_keyscan(%w[-T 5 -p 2222 -f-]) + + cache + + expect(stdin.string).to eq("example.com\n") + end + + context 'successful key scan' do + it 'stores the cleaned known_hosts data' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: "KEY 1\nKEY 1\n\n# comment\nKEY 2\n") + + is_expected.to eq(known_hosts: "KEY 1\nKEY 2\n") + end + end + + context 'failed key scan (exit code 1)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stdout: 'blarg', status: false) + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + + context 'failed key scan (exit code 0)' do + it 'returns a generic error' do + stub_ssh_keyscan(%w[-T 5 -p 2222 -f-], stderr: 'Unknown host') + + is_expected.to eq(error: 'Failed to detect SSH host keys') + end + end + 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/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb new file mode 100644 index 00000000000..e85e7a41017 --- /dev/null +++ b/spec/presenters/blob_presenter_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BlobPresenter, :seed_helper do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + + let(:git_blob) do + Gitlab::Git::Blob.find( + repository, + 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', + 'files/ruby/regex.rb' + ) + end + let(:blob) { Blob.new(git_blob) } + + describe '#highlight' do + subject { described_class.new(blob) } + + it 'returns highlighted content' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil) + + subject.highlight + end + + it 'returns plain content when :plain is true' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil) + + subject.highlight(plain: true) + end + + context 'gitlab-language contains a match' do + before do + allow(blob).to receive(:language_from_gitattributes).and_return('ruby') + end + + it 'passes language to inner call' do + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby') + + subject.highlight + end + end + end +end 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/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru new file mode 100644 index 00000000000..63daeb9eec5 --- /dev/null +++ b/spec/rack_servers/configs/config.ru @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +app = proc do |env| + if env['REQUEST_METHOD'] == 'GET' + [200, {}, ["#{Process.pid}"]] + else + Process.kill(env['QUERY_STRING'], Process.pid) + [200, {}, ['Bye!']] + end +end + +run app diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb new file mode 100644 index 00000000000..d6b6d83d648 --- /dev/null +++ b/spec/rack_servers/configs/puma.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only +# Note: as per the convention in `config/puma.example.development.rb`, +# this file will replace `/home/git` with the actual working directory + +directory '/home/git' +threads 1, 10 +queue_requests false +pidfile '/home/git/gitlab/tmp/pids/puma.pid' +bind 'unix:///home/git/gitlab/tmp/tests/puma.socket' +workers 1 +preload_app! +worker_timeout 60 + +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" +require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer" + +before_fork do + Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options +on_worker_boot do + Gitlab::Cluster::LifecycleEvents.do_worker_start + File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid) +end + +on_restart do + Gitlab::Cluster::LifecycleEvents.do_master_restart +end diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb new file mode 100644 index 00000000000..431fab87857 --- /dev/null +++ b/spec/rack_servers/puma_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'fileutils' + +require 'excon' + +require 'spec_helper' + +describe 'Puma' do + before(:all) do + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('spec/rack_servers/configs/puma.rb') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + + config_path = File.join(project_root, "tmp/tests/puma.rb") + @socket_path = File.join(project_root, 'tmp/tests/puma.socket') + + File.write(config_path, config_lines) + + cmd = %W[puma -e test -C #{config_path} #{File.join(__dir__, 'configs/config.ru')}] + @puma_master_pid = spawn(*cmd) + wait_puma_boot!(@puma_master_pid, File.join(project_root, 'tmp/tests/puma-worker-ready')) + WebMock.allow_net_connect! + end + + %w[SIGQUIT SIGTERM SIGKILL].each do |signal| + it "has a worker that self-terminates on signal #{signal}" do + response = Excon.get('unix://', socket: @socket_path) + expect(response.status).to eq(200) + + worker_pid = response.body.to_i + expect(worker_pid).to be > 0 + + begin + Excon.post("unix://?#{signal}", socket: @socket_path) + rescue Excon::Error::Socket + # The connection may be closed abruptly + end + + expect(pid_gone?(worker_pid)).to eq(true) + end + end + + after(:all) do + begin + WebMock.disable_net_connect!(allow_localhost: true) + Process.kill('TERM', @puma_master_pid) + rescue Errno::ESRCH + end + end + + def wait_puma_boot!(master_pid, ready_file) + # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes. + timeout = 5 * 60 + timeout.times do + return if File.exist?(ready_file) + + pid = Process.waitpid(master_pid, Process::WNOHANG) + raise "puma failed to boot: #{$?}" unless pid.nil? + + sleep 1 + end + + raise "puma boot timed out after #{timeout} seconds" + end + + def pid_gone?(pid) + # Worker termination should take less than a second. That makes 10 + # seconds a generous timeout. + 10.times do + begin + Process.kill(0, pid) + rescue Errno::ESRCH + return true + end + + sleep 1 + end + + false + end +end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb index a4cf479a339..6a02ebcd048 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/rack_servers/unicorn_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fileutils' require 'excon' @@ -6,12 +8,16 @@ require 'spec_helper' describe 'Unicorn' do before(:all) do - config_lines = File.read('config/unicorn.rb.example').split("\n") + project_root = File.expand_path('../..', __dir__) + + config_lines = File.read('config/unicorn.rb.example') + .gsub('/home/git/gitlab', project_root) + .gsub('/home/git', project_root) + .split("\n") # Remove these because they make setup harder. config_lines = config_lines.reject do |line| %w[ - working_directory worker_processes listen pid @@ -26,33 +32,18 @@ describe 'Unicorn' do # predictable which process will handle our requests. config_lines << 'worker_processes 1' - @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket') + @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket') config_lines << "listen '#{@socket_path}'" - ready_file = 'tmp/tests/unicorn-worker-ready' + ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready') FileUtils.rm_f(ready_file) after_fork_index = config_lines.index { |l| l.start_with?('after_fork') } config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)") - config_path = 'tmp/tests/unicorn.rb' + config_path = File.join(project_root, 'tmp/tests/unicorn.rb') File.write(config_path, config_lines.join("\n") + "\n") - rackup_path = 'tmp/tests/config.ru' - File.write(rackup_path, <<~EOS) - app = - proc do |env| - if env['REQUEST_METHOD'] == 'GET' - [200, {}, [Process.pid]] - else - Process.kill(env['QUERY_STRING'], Process.pid) - [200, {}, ['Bye!']] - end - end - - run app - EOS - - cmd = %W[unicorn -E test -c #{config_path} #{rackup_path}] + cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru] @unicorn_master_pid = spawn(*cmd) wait_unicorn_boot!(@unicorn_master_pid, ready_file) WebMock.allow_net_connect! 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/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9f6cf12f9a7..9cda39a569b 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -178,6 +178,24 @@ describe API::Issues do expect(first_issue['id']).to eq(issue2.id) end + it 'returns issues with no assignee' do + issue2 = create(:issue, author: user2, project: project) + + get api('/issues', user), assignee_id: 'None', scope: 'all' + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues with any assignee' do + # This issue without assignee should not be returned + create(:issue, author: user2, project: project) + + get api('/issues', user), assignee_id: 'Any', scope: 'all' + + expect_paginated_array_response(size: 3) + end + it 'returns issues reacted by the authenticated user by the given emoji' do issue2 = create(:issue, project: project, author: user, assignees: [user]) award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star') diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 07d19e3ad29..e4e0ca285e0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -143,6 +143,23 @@ describe API::MergeRequests do expect_response_ordered_exactly(merge_request3) end + it 'returns an array of merge requests with no assignee' do + merge_request3 = create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'None', scope: :all + + expect_response_ordered_exactly(merge_request3) + end + + it 'returns an array of merge requests with any assignee' do + # This MR with no assignee should not be returned + create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + + get api('/merge_requests', user), assignee_id: 'Any', scope: :all + + expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked) + end + it 'returns an array of merge requests assigned to me' do merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 22e5a7c7174..62b6a3ce42e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -200,6 +200,24 @@ describe API::Projects do expect(json_response.first).to include 'statistics' end + it "does not include license by default" do + get api('/projects', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + + it "does not include license if requested" do + get api('/projects', user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first).not_to include('license', 'license_url') + end + context 'when external issue tracker is enabled' do let!(:jira_service) { create(:jira_service, project: project) } @@ -994,6 +1012,26 @@ describe API::Projects do }) end + it "does not include license fields by default" do + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include('license', 'license_url') + end + + it 'includes license fields when requested' do + get api("/projects/#{project.id}", user), license: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['license']).to eq({ + 'key' => project.repository.license.key, + 'name' => project.repository.license.name, + 'nickname' => project.repository.license.nickname, + 'html_url' => project.repository.license.url, + 'source_url' => project.repository.license.meta['source'] + }) + end + it "does not include statistics by default" do get api("/projects/#{project.id}", user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 43ceb332cfb..909703a8d47 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -797,6 +797,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_runner_system_failure } end + + context 'when failure_reason is unrecognized value' do + before do + update_job(state: 'failed', failure_reason: 'what_is_this') + job.reload + end + + 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/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb new file mode 100644 index 00000000000..6baf7ac9deb --- /dev/null +++ b/spec/services/boards/visits/create_service_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::CreateService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(project_board)).to eq nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(project_board)).to eq nil + end + + it 'records the visit' do + expect(BoardProjectRecentVisit).to receive(:visited!).once + + service.execute(project_board) + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(group_board)).to eq nil + end + + it 'records the visit' do + expect(BoardGroupRecentVisit).to receive(:visited!).once + + service.execute(group_board) + end + end + end +end diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb new file mode 100644 index 00000000000..e55d599e2cc --- /dev/null +++ b/spec/services/boards/visits/latest_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Boards::Visits::LatestService do + describe '#execute' do + let(:user) { create(:user) } + + context 'when a project board' do + let(:project) { create(:project) } + let(:project_board) { create(:board, project: project) } + + subject(:service) { described_class.new(project_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardProjectRecentVisit).to receive(:latest).once + + service.execute + end + end + + context 'when a group board' do + let(:group) { create(:group) } + let(:group_board) { create(:board, group: group) } + + subject(:service) { described_class.new(group_board.parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute).to eq nil + end + + it 'queries for most recent visit' do + expect(BoardGroupRecentVisit).to receive(:latest).once + + service.execute + end + end + end +end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index d71ccfb4334..1289d3ce01f 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -177,7 +177,7 @@ describe Groups::TransferService, :postgresql do it 'should add an error on group' do transfer_service.execute(new_parent_group) - expect(transfer_service.error).to eq('Transfer failed: Validation failed: Path has already been taken') + expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken') end end 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/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb index 0d333d541c9..c76f6e6f77e 100644 --- a/spec/services/resource_events/merge_into_notes_service_spec.rb +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -66,5 +66,14 @@ describe ResourceEvents::MergeIntoNotesService do expect(notes.count).to eq 1 expect(notes.first.discussion_id).to eq event.discussion_id end + + it "preloads the note author's status" do + event = create_event(created_at: time) + create(:user_status, user: event.user) + + notes = described_class.new(resource, user).execute + + expect(notes.first.author.association(:status)).to be_loaded + end end end 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/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb index 25781f5e679..90d7f60fdeb 100644 --- a/spec/support/helpers/seed_helper.rb +++ b/spec/support/helpers/seed_helper.rb @@ -1,15 +1,18 @@ +# frozen_string_literal: true + require_relative 'test_env' # This file is specific to specs in spec/lib/gitlab/git/ SEED_STORAGE_PATH = TestEnv.repos_path -TEST_REPO_PATH = 'gitlab-git-test.git'.freeze -TEST_NORMAL_REPO_PATH = 'not-bare-repo.git'.freeze -TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze -TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze +TEST_REPO_PATH = 'gitlab-git-test.git' +TEST_NORMAL_REPO_PATH = 'not-bare-repo.git' +TEST_MUTABLE_REPO_PATH = 'mutable-repo.git' +TEST_BROKEN_REPO_PATH = 'broken-repo.git' +TEST_GITATTRIBUTES_REPO_PATH = 'with-git-attributes.git' module SeedHelper - GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze + GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__) def ensure_seeds if File.exist?(SEED_STORAGE_PATH) @@ -66,6 +69,11 @@ module SeedHelper end def create_git_attributes + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_GITATTRIBUTES_REPO_PATH}), + chdir: SEED_STORAGE_PATH, + out: '/dev/null', + err: '/dev/null') + dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info') FileUtils.mkdir_p(dir) @@ -82,6 +90,8 @@ foo/bar.* foo *.cgi key=value?p1=v1&p2=v2 /*.png gitlab-language=png *.binary binary +/custom-highlighting/*.gitlab-custom gitlab-language=ruby +/custom-highlighting/*.gitlab-cgi gitlab-language=erb?parent=json # This uses a tab instead of spaces to ensure the parser also supports this. *.md\tgitlab-language=markdown 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/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index aedbaa66d34..95f7f87d37b 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -29,6 +29,8 @@ describe 'projects/blob/_viewer.html.haml' do controller.params[:namespace_id] = project.namespace.to_param controller.params[:project_id] = project.to_param controller.params[:id] = File.join('master', blob.path) + + allow(project.repository).to receive(:gitattribute).and_return(nil) end def render_view 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..0124ee0572d 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.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.10.0.tgz#3ac54ecaa25ea558324f0b382c97fcf9e3c4f0a5" + integrity sha512-kfoCKA+AmWZ3hf1wOS8W9mPJs/7lF+a01PK//+sw2MOLv6PlduJJmdN8drFuJ65o6cTJ1f9FMVB80R6D71XVKQ== 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= |