diff options
430 files changed, 4644 insertions, 1659 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3163b687b4..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 @@ -694,7 +694,10 @@ gitlab:setup-mysql: # Frontend-related jobs gitlab:assets:compile: <<: *dedicated-no-docs-and-no-qa-pull-cache-job + image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1 dependencies: [] + services: + - docker:stable-dind variables: NODE_ENV: "production" RAILS_ENV: "production" @@ -703,18 +706,23 @@ gitlab:assets:compile: WEBPACK_REPORT: "true" # we override the max_old_space_size to prevent OOM errors NODE_OPTIONS: --max_old_space_size=3584 + DOCKER_DRIVER: overlay2 + DOCKER_HOST: tcp://docker:2375 script: - date - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - date - free -m - bundle exec rake gitlab:assets:compile + - scripts/build_assets_image artifacts: name: webpack-report expire_in: 31d paths: - webpack-report/ - public/assets/ + tags: + - docker karma: <<: *dedicated-no-docs-pull-cache-job 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/Dockerfile.assets b/Dockerfile.assets new file mode 100644 index 00000000000..403d16cc4ab --- /dev/null +++ b/Dockerfile.assets @@ -0,0 +1,4 @@ +# Simple container to store assets for later use +FROM scratch +ADD public/assets /assets/ +CMD /bin/true 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 @@ -244,9 +244,6 @@ gem 'rack-attack', '~> 4.4.1' # Ace editor gem 'ace-rails-ap', '~> 4.1.0' -# Keyboard shortcuts -gem 'mousetrap-rails', '~> 1.4.6' - # Detect and convert string character encoding gem 'charlock_holmes', '~> 0.7.5' diff --git a/Gemfile.lock b/Gemfile.lock index e533b564d15..e755b0e0a8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -463,7 +463,6 @@ GEM mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) - mousetrap-rails (1.4.6) msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) @@ -547,7 +546,7 @@ GEM orm_adapter (0.5.0) os (1.0.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) parslet (1.8.2) peek (1.0.1) @@ -1046,7 +1045,6 @@ DEPENDENCIES method_source (~> 0.8) mini_magick minitest (~> 5.7.0) - mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap net-ssh (~> 5.0) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index b24911f3bb2..6ae7444f18f 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -466,7 +466,6 @@ GEM mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.7.0) - mousetrap-rails (1.4.6) msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) @@ -1055,7 +1054,6 @@ DEPENDENCIES method_source (~> 0.8) mini_magick minitest (~> 5.7.0) - mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap net-ssh (~> 5.0) diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 030288a1c9d..ae2d1ee3c6e 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import { Button } from '@gitlab-org/gitlab-ui'; +import { GlButton } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; @@ -10,7 +10,7 @@ export default { name: 'BoardNewIssue', components: { ProjectSelect, - 'gl-button': Button, + GlButton, }, props: { groupId: { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 3baac08d411..20665f903d5 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,12 +1,12 @@ <script> -import { Link } from '@gitlab-org/gitlab-ui'; +import { GlLink } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { - 'gl-link': Link, + GlLink, Icon, }, data() { diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js index 1089d0a72d3..c7a917457f3 100644 --- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -47,7 +47,7 @@ export default class AjaxVariableList { } onSaveClicked() { - const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon'); + const loadingIcon = this.saveButton.querySelector('.js-ci-variables-save-loading-icon'); loadingIcon.classList.toggle('hide', false); this.errorBox.classList.toggle('hide', true); // We use this to prevent a user from changing a key before we have a chance diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index 1411f7ffd5e..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/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/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index d3b2656743d..ae0a8c74964 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -9,7 +9,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 8f5ac3d8082..15c6fb550c1 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new AjaxVariableList({ container: variableListEl, - saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + saveButton: variableListEl.querySelector('.js-ci-variables-save-button'), errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); diff --git a/app/assets/javascripts/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/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/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/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/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 5d9f7cebcf2..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,4 +1,6 @@ <script> +import _ from 'underscore'; +import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; @@ -80,6 +82,7 @@ export default { const service = this.createService(store); return { mr: store, + state: store.state, service, }; }, @@ -103,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(); @@ -112,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) { @@ -146,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({ @@ -158,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, @@ -167,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 @@ -199,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; @@ -264,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"> @@ -308,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/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/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index e6655914700..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 @@ -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/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/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1c84baf68ed..c030d75f5a4 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -250,6 +250,100 @@ max-width: 100%; } +/* +* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs) +*/ +@mixin build-trace { + background: $black; + color: $gray-darkest; + white-space: pre; + overflow-x: auto; + font-size: 12px; + border-radius: 0; + border: 0; + padding: $grid-size; + + .bash { + display: block; + } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } +} + +@mixin build-trace-top-bar($height) { + height: $height; + min-height: $height; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; + position: sticky; + position: -webkit-sticky; + top: $header-height; + padding: $grid-size; + + .with-performance-bar & { + top: $header-height + $performance-bar-height; + } +} + +/* +* Mixin that handles the position of the controls placed on the top bar +*/ +@mixin build-controllers($control-font-size, $flex-direction, $with-grow, $flex-grow-size) { + display: flex; + font-size: $control-font-size; + justify-content: $flex-direction; + align-items: center; + align-self: baseline; + @if $with-grow { + flex-grow: $flex-grow-size; + } + + svg { + width: 15px; + height: 15px; + display: block; + fill: $gl-text-color; + } + + .controllers-buttons { + color: $gl-text-color; + margin: 0 $grid-size; + + &:last-child { + margin-right: 0; + } + } + + .btn-scroll.animate { + .first-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.3s; + } + + .second-triangle { + animation: blinking-scroll-button 1s ease infinite; + animation-delay: 0.2s; + } + + .third-triangle { + animation: blinking-scroll-button 1s ease infinite; + } + + &:disabled { + opacity: 1; + } + } + + .btn-scroll:disabled, + .btn-refresh:disabled { + opacity: 0.35; + cursor: not-allowed; + } +} + @mixin build-loader-animation { position: relative; white-space: initial; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 227f49ec595..31b258e56dd 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,35 +50,13 @@ position: relative; } - .build-trace { - background: $black; - color: $gray-darkest; - white-space: pre; - overflow-x: auto; - font-size: 12px; - border-radius: 0; - border: 0; - padding: $grid-size; - - .bash { - display: block; - } - &.build-trace-rounded { - border-radius: $border-radius-base; - } + .build-trace { + @include build-trace(); } .top-bar { - height: 35px; - min-height: 35px; - background: $gray-light; - border: 1px solid $border-color; - color: $gl-text-color; - position: sticky; - position: -webkit-sticky; - top: $header-height; - padding: $grid-size; + @include build-trace-top-bar(35px); &.affix { top: $header-height; @@ -116,49 +94,7 @@ } .controllers { - display: flex; - justify-content: center; - align-items: center; - - svg { - height: 15px; - display: block; - fill: $gl-text-color; - } - - .controllers-buttons { - color: $gl-text-color; - margin: 0 $grid-size; - - &:last-child { - margin-right: 0; - } - } - - .btn-scroll.animate { - .first-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.3s; - } - - .second-triangle { - animation: blinking-scroll-button 1s ease infinite; - animation-delay: 0.2s; - } - - .third-triangle { - animation: blinking-scroll-button 1s ease infinite; - } - - &:disabled { - opacity: 1; - } - } - - .btn-scroll:disabled { - opacity: 0.35; - cursor: not-allowed; - } + @include build-controllers(15px, center, false, 0); } } @@ -183,12 +119,8 @@ } .with-performance-bar .build-page { - .top-bar { + .top-bar.affix { top: $header-height + $performance-bar-height; - - &.affix { - top: $header-height + $performance-bar-height; - } } } diff --git a/app/controllers/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/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b3777fd2b0f..f644702cbdb 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -86,10 +86,10 @@ module CreatesCommit def new_merge_request_path project_new_merge_request_path( @project_to_commit_into, + merge_request_source_branch: @branch_name, merge_request: { source_project_id: @project_to_commit_into.id, target_project_id: @project.id, - source_branch: @branch_name, target_branch: @start_branch } ) diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 6e17bc212e4..3802aa5f40f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController include MilestoneActions before_action :projects + before_action :groups, only: :index before_action :milestone, only: [:show, :merge_requests, :participants, :labels] def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(@projects) + @milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id)) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do @@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController @milestone = DashboardMilestone.build(@projects, params[:title]) render_404 unless @milestone end + + def groups + @groups ||= GroupsFinder.new(current_user, state_all: true).execute + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index a7cee426cf1..b42116b0f36 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -10,7 +10,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(group_projects, group) + @milestone_states = Milestone.states_count(group_projects, [group]) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 93f3eb2be6d..c1dcc463de7 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -7,7 +7,7 @@ module Groups before_action :authorize_admin_pipeline! def show - define_secret_variables + define_ci_variables end def reset_registration_token @@ -19,7 +19,7 @@ module Groups private - def define_secret_variables + def define_ci_variables @variable = Ci::GroupVariable.new(group: group) .present(current_user: current_user) @variables = group.variables.order_key_asc diff --git a/app/controllers/projects/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/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b06a6f3bb0d..308f666394c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -9,12 +9,25 @@ class Projects::IssuesController < Projects::ApplicationController include IssuesCalendar include SpammableActions - prepend_before_action :authenticate_user!, only: [:new] + def self.authenticate_user_only_actions + %i[new] + end + + def self.issue_except_actions + %i[index calendar new create bulk_update] + end + + def self.set_issuables_index_only_actions + %i[index calendar] + end + + prepend_before_action :authenticate_user!, only: authenticate_user_only_actions before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update] - before_action :set_issuables_index, only: [:index, :calendar] + before_action :issue, except: issue_except_actions + + before_action :set_issuables_index, only: set_issuables_index_only_actions # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 5639402a1e9..bbf662a63c8 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -89,6 +89,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap def build_merge_request params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) + params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence + @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 757b03d0b0e..27b83da4f54 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -168,7 +168,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge - return access_denied! unless @merge_request.can_be_merged_by?(current_user) + access_check_result = merge_access_check + + return access_check_result if access_check_result status = merge! @@ -201,9 +203,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def ci_environments_status - environments = @merge_request.environments_for(current_user).map do |environment| - EnvironmentStatus.new(environment, @merge_request) - end + environments = if ci_environments_status_on_merge_result? + EnvironmentStatus.after_merge_request(@merge_request, current_user) + else + EnvironmentStatus.for_merge_request(@merge_request, current_user) + end render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments) end @@ -241,6 +245,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private + def ci_environments_status_on_merge_result? + params[:environment_target] == 'merge_commit' + end + def target_branch_missing? @merge_request.has_no_commits? && !@merge_request.target_branch_exists? end @@ -256,6 +264,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo return :failed end + merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params) + + unless merge_service.hooks_validation_pass?(@merge_request) + return :hook_validation_error + end + return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) @@ -318,6 +332,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo access_denied! unless access_check end + def merge_access_check + access_denied! unless @merge_request.can_be_merged_by?(current_user) + end + def whitelist_query_limiting # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438') diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 3a1344651df..75e590f3f33 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -68,7 +68,7 @@ module Projects def define_variables define_runners_variables - define_secret_variables + define_ci_variables define_triggers_variables define_badges_variables define_auto_devops_variables @@ -90,7 +90,7 @@ module Projects @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id) end - def define_secret_variables + def define_ci_variables @variable = ::Ci::Variable.new(project: project) .present(current_user: current_user) @variables = project.variables.order_key_asc diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index 5beea92689f..81fd3b7a547 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -3,7 +3,7 @@ class PersonalAccessTokensFinder attr_accessor :params - delegate :build, :find, :find_by, to: :execute + delegate :build, :find, :find_by, :find_by_token, to: :execute def initialize(params = {}) @params = params diff --git a/app/helpers/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/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/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_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/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/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/clusters/create_service.rb b/app/services/clusters/create_service.rb index c6e955800af..cd843b8ffa8 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -9,9 +9,9 @@ module Clusters end def execute(project:, access_token: nil) - raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project) + raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project) - cluster_params = params.merge(user: current_user, projects: [project]) + cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project]) cluster_params[:provider_gcp_attributes].try do |provider| provider[:access_token] = access_token end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index ced87a1c37a..80de897e94b 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -24,8 +24,8 @@ class DeleteMergedBranchesService < BaseService # rubocop: disable CodeReuse/ActiveRecord def merge_request_branch_names # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY - source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch) - target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch) + source_names = project.origin_merge_requests.opened.reorder(nil).distinct.pluck(:source_branch) + target_names = project.merge_requests.opened.reorder(nil).distinct.pluck(:target_branch) (source_names + target_names).uniq end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 52360f775dc..9cbc9fef529 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -40,7 +40,7 @@ module Labels group_labels_applied_to_merge_requests ]) .reorder(nil) - .uniq + .distinct end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb index 7c88c9abb41..35a22449e34 100644 --- a/app/services/merge_requests/get_urls_service.rb +++ b/app/services/merge_requests/get_urls_service.rb @@ -50,8 +50,8 @@ module MergeRequests end def url_for_new_merge_request(branch_name) - merge_request_params = { source_branch: branch_name } - url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params) + url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name) + { branch_name: branch_name, url: url, new_merge_request: true } end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index fb44f809c41..70a67baa01c 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -49,6 +49,11 @@ module MergeRequests end end + # Overridden in EE. + def hooks_validation_pass?(_merge_request) + true + end + private def error_check! diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index b03d14fa3cc..f01872b205e 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -85,7 +85,7 @@ module MergeRequests .where.not(target_project: @project).to_a filter_merge_requests(merge_requests).each do |merge_request| - if merge_request.source_branch == @push.branch_name || @push.force_push? + if branch_and_project_match?(merge_request) || @push.force_push? merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commit_shas @@ -104,6 +104,11 @@ module MergeRequests end # rubocop: enable CodeReuse/ActiveRecord + def branch_and_project_match?(merge_request) + merge_request.source_project == @project && + merge_request.source_branch == @push.branch_name + end + def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) end diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index e402801a776..f34305e94fa 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -9,8 +9,8 @@ = render 'ci/variables/variable_row', form_field: 'variables', variable: variable = render 'ci/variables/variable_row', form_field: 'variables' .prepend-top-20 - %button.btn.btn-success.js-secret-variables-save-button{ type: 'button' } - %span.hide.js-secret-variables-save-loading-icon + %button.btn.btn-success.js-ci-variables-save-button{ type: 'button' } + %span.hide.js-ci-variables-save-loading-icon = icon('spinner spin') = _('Save variables') %button.btn.btn-info.btn-inverted.prepend-left-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@variables.size == 0}" } } diff --git a/app/views/groups/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/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/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/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/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/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/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/51527-xss-in-mr-source-branch.yml b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml new file mode 100644 index 00000000000..dae277b6413 --- /dev/null +++ b/changelogs/unreleased/51527-xss-in-mr-source-branch.yml @@ -0,0 +1,5 @@ +--- +title: Fix XSS in merge request source branch name +merge_request: +author: +type: security diff --git a/changelogs/unreleased/52122-fix-broken-whitespace-button.yml b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml new file mode 100644 index 00000000000..3f261eb2ac5 --- /dev/null +++ b/changelogs/unreleased/52122-fix-broken-whitespace-button.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken "Show whitespace changes" button on MRs. +merge_request: 22539 +author: +type: fixed diff --git a/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml new file mode 100644 index 00000000000..adf153f33ce --- /dev/null +++ b/changelogs/unreleased/52383-ui-filter-assignee-none-any.yml @@ -0,0 +1,5 @@ +--- +title: Add None/Any option for assignee_id in search bar +merge_request: 22599 +author: Heinrich Lee Yu +type: added diff --git a/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml new file mode 100644 index 00000000000..7586d7995b7 --- /dev/null +++ b/changelogs/unreleased/52780-stale-pipeline-status-cache-for-_project-after-disabling-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Cache pipeline status per SHA. +merge_request: 22589 +author: +type: fixed diff --git a/changelogs/unreleased/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/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/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml new file mode 100644 index 00000000000..b89ba754952 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml @@ -0,0 +1,5 @@ +--- +title: Make new merge request URL more friendly when pushing code +merge_request: 22526 +author: "@blackst0ne" +type: changed diff --git a/changelogs/unreleased/fix-53298.yml b/changelogs/unreleased/fix-53298.yml new file mode 100644 index 00000000000..f0bf5470dc8 --- /dev/null +++ b/changelogs/unreleased/fix-53298.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix #53298: JupyterHub restarts should work without errors' +merge_request: 22671 +author: Amit Rathi +type: fixed diff --git a/changelogs/unreleased/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/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/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/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/routes.rb b/config/routes.rb index 8723a928cc3..37c7f98ec98 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,7 @@ Rails.application.routes.draw do get 'ide' => 'ide#index' get 'ide/*vueroute' => 'ide#index', format: false + draw :operations draw :instance_statistics end diff --git a/config/routes/project.rb b/config/routes/project.rb index 85872a4122a..73c46f72168 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -149,9 +149,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do scope path: 'merge_requests', controller: 'merge_requests/creations' do post '', action: :create, as: nil - scope path: 'new', as: :new_merge_request do - get '', action: :new - + scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do scope constraints: { format: nil }, action: :new do get :diffs, defaults: { tab: 'diffs' } get :pipelines, defaults: { tab: 'pipelines' } @@ -165,6 +163,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :diff_for_path get :branch_from get :branch_to + get '', action: :new end end diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb new file mode 100644 index 00000000000..203fcfe8eae --- /dev/null +++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column :personal_access_tokens, :token, :string, null: true + + add_column :personal_access_tokens, :token_digest, :string + end + + def down + remove_column :personal_access_tokens, :token_digest + + change_column :personal_access_tokens, :token, :string, null: false + end +end diff --git a/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb new file mode 100644 index 00000000000..4300cd13a45 --- /dev/null +++ b/db/migrate/20180910153413_add_index_to_token_digest_on_personal_access_tokens.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToTokenDigestOnPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :personal_access_tokens, :token_digest, unique: true + end + + def down + remove_concurrent_index :personal_access_tokens, :token_digest if index_exists?(:personal_access_tokens, :token_digest) + end +end diff --git a/db/migrate/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/20181014203236_create_cluster_groups.rb b/db/migrate/20181014203236_create_cluster_groups.rb new file mode 100644 index 00000000000..69382d5c851 --- /dev/null +++ b/db/migrate/20181014203236_create_cluster_groups.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateClusterGroups < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :cluster_groups do |t| + t.references :cluster, null: false, foreign_key: { on_delete: :cascade } + t.references :group, null: false, index: true + + t.index [:cluster_id, :group_id], unique: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + end + end +end diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb new file mode 100644 index 00000000000..191e7eb4fb3 --- /dev/null +++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddClusterTypeToClusters < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + PROJECT_CLUSTER_TYPE = 3 + + disable_ddl_transaction! + + def up + add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE) + end + + def down + remove_column(:clusters, :cluster_type) + end +end diff --git a/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb new file mode 100644 index 00000000000..36be819b245 --- /dev/null +++ b/db/post_migrate/20180913142237_schedule_digest_personal_access_tokens.rb @@ -0,0 +1,28 @@ +class ScheduleDigestPersonalAccessTokens < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + BATCH_SIZE = 10_000 + MIGRATION = 'DigestColumn' + DELAY_INTERVAL = 5.minutes.to_i + + disable_ddl_transaction! + + class PersonalAccessToken < ActiveRecord::Base + include EachBatch + + self.table_name = 'personal_access_tokens' + end + + def up + PersonalAccessToken.where('token is NOT NULL').each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck('MIN(id)', 'MAX(id)').first + BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, ['PersonalAccessToken', :token, :token_digest, *range]) + end + end + + def down + # raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/post_migrate/20181014121030_enqueue_redact_links.rb b/db/post_migrate/20181014121030_enqueue_redact_links.rb new file mode 100644 index 00000000000..1ee4703c88a --- /dev/null +++ b/db/post_migrate/20181014121030_enqueue_redact_links.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class EnqueueRedactLinks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 1000 + DELAY_INTERVAL = 5.minutes.to_i + MIGRATION = 'RedactLinks' + + disable_ddl_transaction! + + class Note < ActiveRecord::Base + include EachBatch + + self.table_name = 'notes' + self.inheritance_column = :_type_disabled + end + + class Issue < ActiveRecord::Base + include EachBatch + + self.table_name = 'issues' + self.inheritance_column = :_type_disabled + end + + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + self.inheritance_column = :_type_disabled + end + + class Snippet < ActiveRecord::Base + include EachBatch + + self.table_name = 'snippets' + self.inheritance_column = :_type_disabled + end + + def up + disable_statement_timeout do + schedule_migration(Note, 'note') + schedule_migration(Issue, 'description') + schedule_migration(MergeRequest, 'description') + schedule_migration(Snippet, 'description') + end + end + + def down + # nothing to do + end + + private + + def schedule_migration(model, field) + link_pattern = "%/sent_notifications/" + ("_" * 32) + "/unsubscribe%" + + model.where("#{field} like ?", link_pattern).each_batch(of: BATCH_SIZE) do |batch, index| + start_id, stop_id = batch.pluck('MIN(id)', 'MAX(id)').first + + BackgroundMigrationWorker.perform_in(index * DELAY_INTERVAL, MIGRATION, [model.name.demodulize, field, start_id, stop_id]) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7a75aafd7b0..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: 20181016152238) do +ActiveRecord::Schema.define(version: 20181017001059) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -599,6 +599,14 @@ ActiveRecord::Schema.define(version: 20181016152238) do add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree + create_table "cluster_groups", force: :cascade do |t| + t.integer "cluster_id", null: false + t.integer "group_id", null: false + end + + add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree + add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree + create_table "cluster_platforms_kubernetes", force: :cascade do |t| t.integer "cluster_id", null: false t.datetime_with_timezone "created_at", null: false @@ -654,6 +662,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do t.boolean "enabled", default: true t.string "name", null: false t.string "environment_scope", default: "*", null: false + t.integer "cluster_type", limit: 2, default: 3, null: false end add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree @@ -1158,6 +1167,7 @@ ActiveRecord::Schema.define(version: 20181016152238) 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| @@ -1539,7 +1549,7 @@ ActiveRecord::Schema.define(version: 20181016152238) do create_table "personal_access_tokens", force: :cascade do |t| t.integer "user_id", null: false - t.string "token", null: false + t.string "token" t.string "name", null: false t.boolean "revoked", default: false t.date "expires_at" @@ -1547,9 +1557,11 @@ ActiveRecord::Schema.define(version: 20181016152238) do t.datetime "updated_at", null: false t.string "scopes", default: "--- []\n", null: false t.boolean "impersonation", default: false, null: false + t.string "token_digest" end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree + add_index "personal_access_tokens", ["token_digest"], name: "index_personal_access_tokens_on_token_digest", unique: true, using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree create_table "programming_languages", force: :cascade do |t| @@ -2372,6 +2384,8 @@ ActiveRecord::Schema.define(version: 20181016152238) do add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade + add_foreign_key "cluster_groups", "clusters", on_delete: :cascade + add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "clusters", on_delete: :cascade add_foreign_key "cluster_projects", "projects", on_delete: :cascade diff --git a/doc/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/logs.md b/doc/administration/logs.md index 0e41a38b7e2..038e043281c 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -84,7 +84,7 @@ Introduced in GitLab 10.0, this file lives in It helps you see requests made directly to the API. For example: ```json -{"time":"2017-10-10T12:30:11.579Z","severity":"INFO","duration":16.84,"db":1.57,"view":15.27,"status":200,"method":"POST","path":"/api/v4/internal/allowed","params":{"action":"git-upload-pack","changes":"_any","gl_repository":null,"project":"root/foobar.git","protocol":"ssh","env":"{}","key_id":"[FILTERED]","secret_token":"[FILTERED]"},"host":"127.0.0.1","ip":"127.0.0.1","ua":"Ruby"} +{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30} ``` This entry above shows an access to an internal endpoint to check whether an diff --git a/doc/api/tags.md b/doc/api/tags.md index f2a3f9f28d2..826900ca518 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -134,7 +134,7 @@ Parameters: "description": "Amazing release. Wow" }, "name": "v1.0.0", - "target: "2695effb5807a22ff3d138d593fd856244e155e7", + "target": "2695effb5807a22ff3d138d593fd856244e155e7", "message": null } ``` diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md index 0f79f7d1b17..bc948dc6ea9 100644 --- a/doc/ci/examples/container_scanning.md +++ b/doc/ci/examples/container_scanning.md @@ -63,4 +63,10 @@ are still maintained, they have been deprecated with GitLab 11.0 and may be remo in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change. +CAUTION: **Caution:** +Starting with GitLab 11.5, Container Scanning feature is licensed under the name `container_scanning`. +While the old name `sast_container` is still maintained, it has been deprecated with GitLab 11.5 and +may be removed in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` +configuration to reflect that change if you are using the `$GITLAB_FEATURES` environment variable. + [ee]: https://about.gitlab.com/pricing/ diff --git a/doc/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 4292a17bfa5..4f4ff85fe1d 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -37,9 +37,9 @@ Review Apps are automatically deployed by each pipeline, both in 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. +- 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/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/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/issue_board.md b/doc/user/project/issue_board.md index f5ea350a58f..9e2434c02ec 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -176,8 +176,8 @@ Clicking on the current board name in the upper left corner will reveal a menu from where you can create another Issue Board and rename or delete the existing one. -Clicking on the main issue board link will take you to the last board -you visited. +When you're revisiting an issue board in a project or group with multiple boards, +GitLab will automatically load the last board you visited. NOTE: **Note:** The Multiple Issue Boards feature is available for diff --git a/doc/user/project/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/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/issues.rb b/lib/api/issues.rb index 405fc30a2ed..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 = {}) @@ -46,9 +55,11 @@ module API 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' @@ -57,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/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/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb index d7fe012883d..8e2358694d4 100644 --- a/lib/banzai/filter/issuable_state_filter.rb +++ b/lib/banzai/filter/issuable_state_filter.rb @@ -18,7 +18,7 @@ module Banzai issuables = extractor.extract([doc]) issuables.each do |node, issuable| - next if !can_read_cross_project? && issuable.project != project + next if !can_read_cross_project? && cross_reference?(issuable) if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable) node.content += " (#{issuable.state})" @@ -31,7 +31,14 @@ module Banzai private def issuable_reference?(text, issuable) - text == issuable.reference_link_text(project || group) + CGI.unescapeHTML(text) == issuable.reference_link_text(project || group) + end + + def cross_reference?(issuable) + return true if issuable.project != project + return true if issuable.respond_to?(:group) && issuable.group != group + + false end def can_read_cross_project? diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb index 0a05d46db4c..341dbb74fe0 100644 --- a/lib/banzai/issuable_extractor.rb +++ b/lib/banzai/issuable_extractor.rb @@ -9,13 +9,11 @@ module Banzai # so we can avoid N+1 queries problem class IssuableExtractor - QUERY = %q( - descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")] - [@data-reference-type="issue" or @data-reference-type="merge_request"] - ).freeze - attr_reader :context + ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"'.freeze + MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"'.freeze + # context - An instance of Banzai::RenderContext. def initialize(context) @context = context @@ -24,21 +22,38 @@ module Banzai # Returns Hash in the form { node => issuable_instance } def extract(documents) nodes = documents.flat_map do |document| - document.xpath(QUERY) + document.xpath(query) end - issue_parser = Banzai::ReferenceParser::IssueParser.new(context) + # The project or group for the issuable might be pending for deletion! + # Filter them out because we don't care about them. + issuables_for_nodes(nodes).select { |node, issuable| issuable.project || issuable.group } + end + + private - merge_request_parser = + def issuables_for_nodes(nodes) + parsers.each_with_object({}) do |parser, result| + result.merge!(parser.records_for_nodes(nodes)) + end + end + + def parsers + [ + Banzai::ReferenceParser::IssueParser.new(context), Banzai::ReferenceParser::MergeRequestParser.new(context) + ] + end - issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge( - merge_request_parser.records_for_nodes(nodes) + def query + %Q( + descendant-or-self::a[contains(concat(" ", @class, " "), " gfm ")] + [#{reference_types.join(' or ')}] ) + end - # The project for the issue/MR might be pending for deletion! - # Filter them out because we don't care about them. - issuables_for_nodes.select { |node, issuable| issuable.project } + def reference_types + [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE] end end end 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/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/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/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/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/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/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/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,7 +39,6 @@ module QA module Factory autoload :ApiFabricator, 'qa/factory/api_fabricator' autoload :Base, 'qa/factory/base' - autoload :Product, 'qa/factory/product' module Resource autoload :Sandbox, 'qa/factory/resource/sandbox' @@ -53,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' @@ -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 cfce096ab39..42077f60611 100644 --- a/qa/qa/factory/README.md +++ b/qa/qa/factory/README.md @@ -88,44 +88,6 @@ end The [`Project` factory](./resource/project.rb) is a good real example of Browser UI and API implementations. -### Define attributes - -After the resource is fabricated, we would like to access the attributes on -the resource. We define the attributes with `attribute` method. Suppose -we want to access the name on the resource, we could change `attr_accessor` -to `attribute`: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attribute :name - - # ... same as before - end - end - end -end -``` - -The difference between `attr_accessor` and `attribute` is that by using -`attribute` it can also be accessed from the product: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.name # => "GitLab QA" -``` - -In the above example, if we use `attr_accessor :name` then `shirt.name` won't -be available. On the other hand, using `attribute :name` will allow you to use -`shirt.name`, so most of the time you'll want to use `attribute` instead of -`attr_accessor` unless we clearly don't need it for the product. - #### Resource attributes A resource may need another resource to exist first. For instance, a project @@ -145,7 +107,7 @@ module QA module Factory module Resource class Shirt < Factory::Base - attribute :name + attr_accessor :name attribute :project do Factory::Resource::Project.fabricate! do |resource| @@ -206,7 +168,7 @@ module QA module Factory module Resource class Shirt < Factory::Base - attribute :name + attr_accessor :name attribute :project do Factory::Resource::Project.fabricate! do |resource| @@ -287,7 +249,7 @@ module QA shirt_new.create_shirt! end - brand # Eagerly construct the data + populate(:brand) # Eagerly construct the data end end end @@ -295,9 +257,12 @@ module QA end ``` -This will make sure we construct the data right after we created the shirt. -The drawback for this will become we're forced to construct the data even -if we don't really need to use it. +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. + +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. Alternatively, we could just make sure we're on the right page before constructing the brand data: @@ -307,7 +272,7 @@ module QA module Factory module Resource class Shirt < Factory::Base - attribute :name + attr_accessor :name attribute :project do Factory::Resource::Project.fabricate! do |resource| @@ -385,7 +350,7 @@ end **Notes on attributes precedence:** -- attributes from the factory have the highest precedence +- factory instance variables have the highest precedence - attributes from the API response take precedence over attributes from the block (usually from Browser UI) - attributes without a value will raise a `QA::Factory::Base::NoValueError` error diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index e82e16f9415..e28a00c545b 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -22,12 +22,16 @@ module QA 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 product #{name} of factory #{self.class.name}." unless value + raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value value end @@ -84,7 +88,7 @@ module QA resource_web_url = yield factory.web_url = resource_web_url - Factory::Product.new(factory) + factory end private_class_method :do_fabricate! diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb deleted file mode 100644 index 34df0bda8e5..00000000000 --- a/qa/qa/factory/product.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'capybara/dsl' - -module QA - module Factory - class Product - include Capybara::DSL - - attr_reader :factory - - def initialize(factory) - @factory = factory - - define_attributes - end - - def visit! - visit(web_url) - end - - def populate(*attributes) - attributes.each(&method(:public_send)) - end - - private - - def define_attributes - factory.class.attributes_names.each do |name| - define_singleton_method(name) do - factory.public_send(name) - end - end - end - end - end -end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb index a9dfbc0a783..272b7fc5818 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/factory/repository/project_push.rb @@ -9,8 +9,6 @@ module QA end end - attribute :output - 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/resource/secret_variable.rb b/qa/qa/factory/resource/ci_variable.rb index 24ba3408810..a0aefc61f9f 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/ci_variable.rb @@ -1,13 +1,13 @@ module QA module Factory module Resource - class SecretVariable < Factory::Base + class CiVariable < Factory::Base attr_accessor :key, :value attribute :project do Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-secret-variables' - resource.description = 'project for adding secret variable test' + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' end end @@ -17,7 +17,7 @@ module QA Page::Project::Menu.perform(&:click_ci_cd_settings) Page::Project::Settings::CICD.perform do |setting| - setting.expand_secret_variables do |page| + setting.expand_ci_variables do |page| page.fill_variable(key, value) page.save_variables diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 0fac4377040..b1e874af893 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -50,8 +50,7 @@ module QA end def fabricate! - push - user + populate(:push, :user) visit_project_with_retry diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index 92b8bdf4a21..4b7d2287f98 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -12,8 +12,6 @@ module QA :milestone, :labels - attribute :source_branch - attribute :project do Factory::Resource::Project.fabricate! do |resource| resource.name = 'project-with-merge-request' @@ -52,8 +50,8 @@ module QA end def fabricate! - target - source + populate(:target, :source) + project.visit! Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform do |page| diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb index fbe062539b9..1311bf625a6 100644 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ b/qa/qa/factory/resource/merge_request_from_fork.rb @@ -18,8 +18,10 @@ module QA end def fabricate! - push + populate(:push) + fork.visit! + Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform(&:create_merge_request) end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb index f62092ae122..ce20641e6cc 100644 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ b/qa/qa/factory/resource/project_imported_from_github.rb @@ -4,14 +4,13 @@ module QA module Factory module Resource class ProjectImportedFromGithub < Resource::Project + attr_accessor :name attr_writer :personal_access_token, :github_repository_path attribute :group do Factory::Resource::Group.fabricate! end - attribute :name - def fabricate! group.visit! diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index cfda58dc103..383f534c12c 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -2,14 +2,13 @@ module QA module Factory module Resource class ProjectMilestone < Factory::Base + attr_reader :title attr_accessor :description attribute :project do Factory::Resource::Project.fabricate! end - attribute :title - def title=(title) @title = "#{title}-#{SecureRandom.hex(4)}" @description = 'A milestone' diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index 56bcda9e2f3..a125bac65dd 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -9,7 +9,6 @@ module QA attr_reader :path attribute :id - attribute :path def initialize @path = Runtime::Namespace.sandbox_name diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index a48a93fbe65..6f952eda36f 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -6,11 +6,9 @@ module QA class SSHKey < Factory::Base extend Forwardable - def_delegators :key, :private_key, :public_key, :fingerprint + attr_accessor :title - attribute :private_key - attribute :title - attribute :fingerprint + def_delegators :key, :private_key, :public_key, :fingerprint def key @key ||= Runtime::Key::RSA.new diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index 6e6f46f7a95..e361face1f0 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -5,6 +5,7 @@ module QA module Resource class User < Factory::Base attr_reader :unique_id + attr_writer :username, :password def initialize @unique_id = SecureRandom.hex(8) @@ -30,11 +31,6 @@ module QA defined?(@username) && defined?(@password) end - attribute :name - attribute :username - attribute :email - attribute :password - def fabricate! # Don't try to log-out if we're not logged-in if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } 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/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/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/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 292f24d9c0d..58b272adcf1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -2,24 +2,24 @@ module QA context 'Verify' do - describe 'Secret variable support' do - it 'user adds a secret variable' do + describe 'CI variable support' do + it 'user adds a CI variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' - resource.value = 'some secret variable' + resource.value = 'some CI variable' end Page::Project::Settings::CICD.perform do |settings| - settings.expand_secret_variables do |page| + settings.expand_ci_variables do |page| expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some secret variable') + expect(page).not_to have_field(with: 'some CI variable') page.reveal_variables - expect(page).to have_field(with: 'some secret variable') + expect(page).to have_field(with: 'some CI variable') end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index caf014c89ea..604641e54b8 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 40cae0793dd..c2fce1e7df1 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -22,7 +22,7 @@ module QA # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::SecretVariable.fabricate! do |resource| + Factory::Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index d7b92052894..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(:new).with(factory).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 @@ -140,44 +138,44 @@ describe QA::Factory::Base do describe '.attribute' do include_context 'simple factory' - it 'appends new product attribute' do + it 'appends new attribute' do expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) end - context 'when the product attribute is populated via a block' do - it 'returns a fabrication product and defines factory attributes as its methods' do + 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(QA::Factory::Product) + expect(result).to be_a(described_class) expect(result.test).to eq('block') end end - context 'when the product attribute is populated via the api' do + 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 a fabrication product and defines factory attributes as its methods' do + it 'returns value from api' do result = subject.fabricate!(factory: factory) - expect(result).to be_a(QA::Factory::Product) + expect(result).to be_a(described_class) expect(result.no_block).to eq('api') end - context 'when the attribute also has a block in the factory' do + 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 the api value and emits an INFO log entry' do + it 'returns value from api and emits an INFO log entry' do result = subject.fabricate!(factory: factory) - expect(result).to be_a(QA::Factory::Product) + 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/) @@ -185,15 +183,15 @@ describe QA::Factory::Base do end end - context 'when the product attribute is populated via a factory attribute' do + context 'when the attribute is populated via direct assignment' do before do factory.test = 'value' end - it 'returns a fabrication product and defines factory attributes as its methods' do + it 'returns value from the assignment' do result = subject.fabricate!(factory: factory) - expect(result).to be_a(QA::Factory::Product) + expect(result).to be_a(described_class) expect(result.test).to eq('value') end @@ -202,21 +200,21 @@ describe QA::Factory::Base do allow(factory).to receive(:api_resource).and_return({ test: 'api' }) end - it 'returns the factory attribute for the product' do + it 'returns value from the assignment' do result = subject.fabricate!(factory: factory) - expect(result).to be_a(QA::Factory::Product) + expect(result).to be_a(described_class) expect(result.test).to eq('value') end end end - context 'when the product attribute has no value' do + context 'when the attribute has no value' do it 'raises an error because no values could be found' do result = subject.fabricate!(factory: factory) expect { result.no_block } - .to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.") + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.") end end end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb deleted file mode 100644 index 5b6eaa13e9c..00000000000 --- a/qa/spec/factory/product_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -describe QA::Factory::Product do - let(:factory) do - Class.new(QA::Factory::Base) do - attribute :test do - 'block' - end - - attribute :no_block - end.new - end - - let(:product) { spy('product') } - let(:product_location) { 'http://product_location' } - - subject { described_class.new(factory) } - - before do - factory.web_url = product_location - 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/scripts/build_assets_image b/scripts/build_assets_image new file mode 100755 index 00000000000..218606b9a40 --- /dev/null +++ b/scripts/build_assets_image @@ -0,0 +1,21 @@ +#!/bin/bash + +# Generate the image name based on the project this is being run in +ASSETS_IMAGE_NAME=$(echo ${CI_PROJECT_NAME} | + awk '{ + split($1, p, "-"); + interim = sprintf("%s-assets-%s", p[1], p[2]); + sub(/-$/, "", interim); + print interim + }' +) + +ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME} + +mkdir -p assets_container.build/public +cp -r public/assets assets_container.build/public/ +cp Dockerfile.assets assets_container.build/ +docker build -t ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME} -f assets_container.build/Dockerfile.assets assets_container.build/ +docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} +docker push ${ASSETS_IMAGE_PATH} + diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index be3fc832008..4e91068ab88 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -792,4 +792,30 @@ describe ApplicationController do end end end + + context 'control headers' do + controller(described_class) do + def index + render json: :ok + end + end + + context 'user not logged in' do + it 'sets the default headers' do + get :index + + expect(response.headers['Cache-Control']).to be_nil + end + end + + context 'user logged in' do + it 'sets the default headers' do + sign_in(user) + + get :index + + expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store' + end + end + end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 56047c0c8d2..278b980b6d8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do end describe "#index" do + render_views + it 'returns group and project milestones to which the user belongs' do get :index, format: :json @@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id]) expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end + + it 'should contain group and project milestones to which the user belongs to' do + get :index + + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") + end end end diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb index d1d08391164..f7a4a4192d6 100644 --- a/spec/controllers/groups/boards_controller_spec.rb +++ b/spec/controllers/groups/boards_controller_spec.rb @@ -37,7 +37,7 @@ describe Groups::BoardsController do allow(visited).to receive(:board_id).and_return(12) allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited) - list_boards + list_boards format: :html expect(response).to render_template :index expect(response.content_type).to eq 'text/html' diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 28f7e4634a5..64b589a6d83 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -331,10 +331,10 @@ describe Projects::BlobController do expect(response).to redirect_to( project_new_merge_request_path( forked_project, + merge_request_source_branch: "fork-test-1", merge_request: { source_project_id: forked_project.id, target_project_id: project.id, - source_branch: "fork-test-1", target_branch: "master" } ) diff --git a/spec/controllers/projects/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/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index bcf289f36a9..6420b70a54f 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -5,6 +5,17 @@ shared_examples 'content not cached without revalidation' do end end +shared_examples 'content not cached without revalidation and no-store' do + it 'ensures content will not be cached without revalidation' do + # Fixed in newer versions of ActivePack, it will only output a single `private`. + if Gitlab.rails5? + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store') + else + expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate, private, no-store') + end + end +end + describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -177,7 +188,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' @@ -239,7 +250,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -292,7 +303,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' @@ -344,7 +355,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -388,7 +399,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' @@ -445,7 +456,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' @@ -498,7 +509,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content not cached without revalidation and no-store' do subject do get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index bbeba8ce8b9..c9f5d0a813e 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -2,13 +2,28 @@ FactoryBot.define do factory :cluster, class: Clusters::Cluster do user name 'test-cluster' + cluster_type :project_type + + trait :instance do + cluster_type { Clusters::Cluster.cluster_types[:instance_type] } + end trait :project do + cluster_type { Clusters::Cluster.cluster_types[:project_type] } + before(:create) do |cluster, evaluator| cluster.projects << create(:project, :repository) end end + trait :group do + cluster_type { Clusters::Cluster.cluster_types[:group_type] } + + before(:create) do |cluster, evalutor| + cluster.groups << create(:group) + end + end + trait :provided_by_user do provider_type :user platform_type :kubernetes diff --git a/spec/features/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/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/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/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/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/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js index 026a0c7ea09..3483b7d387d 100644 --- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -11,16 +11,6 @@ describe('collapsedGroupedDatePicker', () => { }); }); - it('should render toggle sidebar if showToggleSidebar', (done) => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); - - vm.showToggleSidebar = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); - done(); - }); - }); - describe('toggleCollapse events', () => { beforeEach((done) => { spyOn(vm, 'toggleSidebar'); @@ -28,12 +18,6 @@ describe('collapsedGroupedDatePicker', () => { Vue.nextTick(done); }); - it('should emit when sidebar is toggled', () => { - vm.$el.querySelector('.gutter-toggle').click(); - - expect(vm.toggleSidebar).toHaveBeenCalled(); - }); - it('should emit when collapsed-calendar-icon is clicked', () => { vm.$el.querySelector('.sidebar-collapsed-icon').click(); @@ -92,5 +76,11 @@ describe('collapsedGroupedDatePicker', () => { expect(icons.length).toEqual(1); expect(icons[0].innerText.trim()).toEqual('None'); }); + + it('should have tooltip as `Start and due date`', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + + expect(icons[0].dataset.originalTitle).toBe('Start and due date'); + }); }); }); diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb new file mode 100644 index 00000000000..3e107ac3027 --- /dev/null +++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913142237 do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + subject { described_class.new } + + describe '#perform' do + context 'token is not yet hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + end + + it 'saves token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token_digest }.from(nil).to(Gitlab::CryptoHelper.sha256('token-01'))) + end + + it 'erases token' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to( + change { PersonalAccessToken.find(1).token }.from('token-01').to(nil)) + end + end + + context 'token is already hashed' do + before do + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token_digest: 'token-digest-01') + end + + it 'does not change existing token digest' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token_digest }) + end + + it 'leaves token empty' do + expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to( + change { PersonalAccessToken.find(1).token }.from(nil)) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/redact_links_spec.rb b/spec/lib/gitlab/background_migration/redact_links_spec.rb new file mode 100644 index 00000000000..a40e68069cc --- /dev/null +++ b/spec/lib/gitlab/background_migration/redact_links_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::RedactLinks, :migration, schema: 20181014121030 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:merge_requests) { table(:merge_requests) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + def create_merge_request(id, params) + params.merge!(id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}") + + merge_requests.create(params) + end + + def create_issue(id, params) + params.merge!(id: id, title: "issue#{id}", project_id: project.id) + + issues.create(params) + end + + def create_note(id, params) + params[:id] = id + + notes.create(params) + end + + def create_snippet(id, params) + params.merge!(id: id, author_id: user.id) + + snippets.create(params) + end + + def create_resource(model, id, params) + send("create_#{model.name.underscore}", id, params) + end + + shared_examples_for 'redactable resource' do + it 'updates only matching texts' do + matching_text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + redacted_text = 'some text /sent_notifications/REDACTED/unsubscribe more text' + create_resource(model, 1, { field => matching_text }) + create_resource(model, 2, { field => 'not matching text' }) + create_resource(model, 3, { field => matching_text }) + create_resource(model, 4, { field => redacted_text }) + create_resource(model, 5, { field => matching_text }) + + expected = { field => 'some text /sent_notifications/REDACTED/unsubscribe more text', + "#{field}_html" => nil } + expect_any_instance_of("Gitlab::BackgroundMigration::RedactLinks::#{model}".constantize).to receive(:update_columns).with(expected).and_call_original + + subject.perform(model, field, 2, 4) + + expect(model.where(field => matching_text).pluck(:id)).to eq [1, 5] + expect(model.find(3).reload[field]).to eq redacted_text + end + end + + context 'resource is Issue' do + it_behaves_like 'redactable resource' do + let(:model) { Issue } + let(:field) { :description } + end + end + + context 'resource is Merge Request' do + it_behaves_like 'redactable resource' do + let(:model) { MergeRequest } + let(:field) { :description } + end + end + + context 'resource is Note' do + it_behaves_like 'redactable resource' do + let(:model) { Note } + let(:field) { :note } + end + end + + context 'resource is Snippet' do + it_behaves_like 'redactable resource' do + let(:model) { Snippet } + let(:field) { :description } + end + end +end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index 4d5081b0a75..e5999a1c509 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -282,6 +282,21 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do expect(pipeline_status.status).to eq(status) expect(pipeline_status.ref).to eq(ref) end + + context 'when status is empty string' do + before do + Gitlab::Redis::Cache.with do |redis| + redis.mapped_hmset(cache_key, + { sha: sha, status: '', ref: ref }) + end + end + + it 'reads the status as nil' do + pipeline_status.load_from_cache + + expect(pipeline_status.status).to eq(nil) + end + end end describe '#has_cache?' do diff --git a/spec/lib/gitlab/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/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/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/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 624e2add860..8df0facdab3 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -29,6 +29,16 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true end + it 'returns true for localhost IPs' do + expect(described_class.blocked_url?('https://0.0.0.0/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://[::1]/foo/foo.git')).to be true + expect(described_class.blocked_url?('https://127.0.0.1/foo/foo.git')).to be true + end + + it 'returns true for loopback IP' do + expect(described_class.blocked_url?('https://127.0.0.2/foo/foo.git')).to be true + end + it 'returns true for alternative version of 127.0.0.1 (0177.1)' do expect(described_class.blocked_url?('https://0177.1:65535/foo/foo.git')).to be true end @@ -84,6 +94,16 @@ describe Gitlab::UrlBlocker do end end + it 'allows localhost endpoints' do + expect(described_class).not_to be_blocked_url('http://0.0.0.0', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://localhost', allow_localhost: true) + expect(described_class).not_to be_blocked_url('http://127.0.0.1', allow_localhost: true) + end + + it 'allows loopback endpoints' do + expect(described_class).not_to be_blocked_url('http://127.0.0.2', allow_localhost: true) + end + it 'allows IPv4 link-local endpoints' do expect(described_class).not_to be_blocked_url('http://169.254.169.254') expect(described_class).not_to be_blocked_url('http://169.254.168.100') @@ -122,7 +142,7 @@ describe Gitlab::UrlBlocker do end def stub_domain_resolv(domain, ip) - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false)]) + allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)]) end def unstub_domain_resolv diff --git a/spec/migrations/enqueue_redact_links_spec.rb b/spec/migrations/enqueue_redact_links_spec.rb new file mode 100644 index 00000000000..a5da76977b7 --- /dev/null +++ b/spec/migrations/enqueue_redact_links_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181014121030_enqueue_redact_links.rb') + +describe EnqueueRedactLinks, :migration, :sidekiq do + let(:merge_requests) { table(:merge_requests) } + let(:issues) { table(:issues) } + let(:notes) { table(:notes) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:snippets) { table(:snippets) } + let(:users) { table(:users) } + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + + text = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' + group = namespaces.create!(name: 'gitlab', path: 'gitlab') + project = projects.create!(namespace_id: group.id) + + merge_requests.create!(id: 1, target_project_id: project.id, source_project_id: project.id, target_branch: 'feature', source_branch: 'master', description: text) + issues.create!(id: 1, description: text) + notes.create!(id: 1, note: text) + notes.create!(id: 2, note: text) + snippets.create!(id: 1, description: text, author_id: user.id) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Note", "note", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, "Note", "note", 2, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Issue", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "MergeRequest", "description", 1, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, "Snippet", "description", 1, 1) + expect(BackgroundMigrationWorker.jobs.size).to eq 5 + end + end + end +end diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb new file mode 100644 index 00000000000..6d155f78342 --- /dev/null +++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb') + +describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do + let(:personal_access_tokens) { table(:personal_access_tokens) } + let(:users) { table(:users) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 4) + + users.create(id: 1, email: 'user@example.com', projects_limit: 10) + + personal_access_tokens.create!(id: 1, user_id: 1, name: 'pat-01', token: 'token-01') + personal_access_tokens.create!(id: 2, user_id: 1, name: 'pat-02', token: 'token-02') + personal_access_tokens.create!(id: 3, user_id: 1, name: 'pat-03', token_digest: 'token_digest') + personal_access_tokens.create!(id: 4, user_id: 1, name: 'pat-04', token: 'token-04') + personal_access_tokens.create!(id: 5, user_id: 1, name: 'pat-05', token: 'token-05') + personal_access_tokens.create!(id: 6, user_id: 1, name: 'pat-06', token: 'token-06') + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + migrate! + + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 5.minutes, 'PersonalAccessToken', 'token', 'token_digest', 1, 5)) + expect(described_class::MIGRATION).to( + be_scheduled_delayed_migration( + 10.minutes, 'PersonalAccessToken', 'token', 'token_digest', 6, 6)) + expect(BackgroundMigrationWorker.jobs.size).to eq 2 + end + end + + it 'schedules background migrations' do + perform_enqueued_jobs do + plain_text_token = 'token IS NOT NULL' + + expect(personal_access_tokens.where(plain_text_token).count).to eq 5 + + migrate! + + expect(personal_access_tokens.where(plain_text_token).count).to eq 0 + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a046541031e..65e06f27f35 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2027,17 +2027,17 @@ describe Ci::Build do it { is_expected.to include(tag_variable) } end - context 'when secret variable is defined' do - let(:secret_variable) do + context 'when CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_variable, - secret_variable.slice(:key, :value).merge(project: project)) + ci_variable.slice(:key, :value).merge(project: project)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when protected variable is defined' do @@ -2072,17 +2072,17 @@ describe Ci::Build do end end - context 'when group secret variable is defined' do - let(:secret_variable) do + context 'when group CI variable is defined' do + let(:ci_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do create(:ci_group_variable, - secret_variable.slice(:key, :value).merge(group: group)) + ci_variable.slice(:key, :value).merge(group: group)) end - it { is_expected.to include(secret_variable) } + it { is_expected.to include(ci_variable) } end context 'when group protected variable is defined' do @@ -2357,7 +2357,7 @@ describe Ci::Build do .to receive(:predefined_variables) { [project_pre_var] } allow_any_instance_of(Project) - .to receive(:secret_variables_for) + .to receive(:ci_variables_for) .with(ref: 'master', environment: nil) do [create(:ci_variable, key: 'secret', value: 'value')] end @@ -2508,7 +2508,7 @@ describe Ci::Build do end describe '#scoped_variables_hash' do - context 'when overriding secret variables' do + context 'when overriding CI variables' do before do project.variables.create!(key: 'MY_VAR', value: 'my value 1') pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2') diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f5c4b0b66ae..c245e8df815 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' describe Clusters::Cluster do it { is_expected.to belong_to(:user) } + it { is_expected.to have_many(:cluster_projects) } it { is_expected.to have_many(:projects) } + it { is_expected.to have_many(:cluster_groups) } + it { is_expected.to have_many(:groups) } it { is_expected.to have_one(:provider_gcp) } it { is_expected.to have_one(:platform_kubernetes) } it { is_expected.to have_one(:application_helm) } @@ -178,6 +181,53 @@ describe Clusters::Cluster do it { expect(cluster.update(enabled: false)).to be_truthy } end end + + describe 'cluster_type validations' do + let(:instance_cluster) { create(:cluster, :instance) } + let(:group_cluster) { create(:cluster, :group) } + let(:project_cluster) { create(:cluster, :project) } + + it 'validates presence' do + cluster = build(:cluster, :project, cluster_type: nil) + + expect(cluster).not_to be_valid + expect(cluster.errors.full_messages).to include("Cluster type can't be blank") + end + + context 'project_type cluster' do + it 'does not allow setting group' do + project_cluster.groups << build(:group) + + expect(project_cluster).not_to be_valid + expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + end + + context 'group_type cluster' do + it 'does not allow setting project' do + group_cluster.projects << build(:project) + + expect(group_cluster).not_to be_valid + expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + + context 'instance_type cluster' do + it 'does not allow setting group' do + instance_cluster.groups << build(:group) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned') + end + + it 'does not allow setting project' do + instance_cluster.projects << build(:project) + + expect(instance_cluster).not_to be_valid + expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned') + end + end + end end describe '#provider' do @@ -229,6 +279,23 @@ describe Clusters::Cluster do end end + describe '#group' do + subject { cluster.group } + + context 'when cluster belongs to a group' do + let(:cluster) { create(:cluster, :group) } + let(:group) { cluster.groups.first } + + it { is_expected.to eq(group) } + end + + context 'when cluster does not belong to any group' do + let(:cluster) { create(:cluster) } + + it { is_expected.to be_nil } + end + end + describe '#applications' do set(:cluster) { create(:cluster) } diff --git a/spec/models/clusters/group_spec.rb b/spec/models/clusters/group_spec.rb new file mode 100644 index 00000000000..ba145342cb8 --- /dev/null +++ b/spec/models/clusters/group_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Group do + it { is_expected.to belong_to(:cluster) } + it { is_expected.to belong_to(:group) } +end diff --git a/spec/models/concerns/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_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/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/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 2ebcb787d06..5ea869796b0 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -683,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 @@ -704,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 @@ -837,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/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/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/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/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/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= |