diff options
424 files changed, 5373 insertions, 1863 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36108d04e9c..6b76853f56f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,6 +9,7 @@ stages: - review - qa - post-test + - notification - pages variables: @@ -33,6 +34,7 @@ include: - local: .gitlab/ci/frontend.gitlab-ci.yml - local: .gitlab/ci/global.gitlab-ci.yml - local: .gitlab/ci/memory.gitlab-ci.yml + - local: .gitlab/ci/notifications.gitlab-ci.yml - local: .gitlab/ci/pages.gitlab-ci.yml - local: .gitlab/ci/qa.gitlab-ci.yml - local: .gitlab/ci/reports.gitlab-ci.yml diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index d746d8fe030..c1cab844ca6 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -93,7 +93,7 @@ - "config.ru" - "{package.json,yarn.lock}" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - - "doc/api/graphql/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated .backstage-patterns: &backstage-patterns - "Dangerfile" @@ -139,7 +139,7 @@ - "config.ru" - "{package.json,yarn.lock}" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - - "doc/api/graphql/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated # Backstage changes - "Dangerfile" - "danger/**/*" @@ -163,7 +163,7 @@ - "config.ru" - "{package.json,yarn.lock}" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - - "doc/api/graphql/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated # QA changes - ".dockerignore" - "qa/**/*" @@ -183,7 +183,7 @@ - "config.ru" - "{package.json,yarn.lock}" - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - - "doc/api/graphql/**/*" + - "doc/api/graphql/reference/*" # Files in this folder are auto-generated # Backstage changes - "Dangerfile" - "danger/**/*" diff --git a/.gitlab/ci/notifications.gitlab-ci.yml b/.gitlab/ci/notifications.gitlab-ci.yml new file mode 100644 index 00000000000..8e2574fad4c --- /dev/null +++ b/.gitlab/ci/notifications.gitlab-ci.yml @@ -0,0 +1,18 @@ +.notify: + image: alpine + stage: notification + dependencies: [] + cache: {} + before_script: + - apk update && apk add git curl bash + variables: + COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list" + +schedule:package-and-qa:notify-failure: + extends: + - .only:variables_refs-canonical-dot-com-schedules + - .notify + script: + - 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_failing' + needs: ["schedule:package-and-qa"] + when: on_failure diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 4ed9ac03d0c..142f0e1c9d4 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -94,10 +94,7 @@ schedule:review-build-cng: variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" - # v2.4.4 + two improvements: - # - Allow to pass an EE license when installing the chart: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1008 - # - Allow to customize the livenessProbe for `gitlab-shell`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1021 - GITLAB_HELM_CHART_REF: "6c655ed77e60f1f7f533afb97bef8c9cb7dc61eb" + GITLAB_HELM_CHART_REF: "v2.5.1" GITLAB_EDITION: "ce" environment: name: review/${CI_COMMIT_REF_NAME} @@ -135,13 +132,11 @@ review-deploy: - .review-deploy-base - .only-review - .only:changes-code-qa - needs: ["review-build-cng"] schedule:review-deploy: extends: - .review-deploy-base - .only-review-schedules - needs: ["schedule:review-build-cng"] .base-review-stop: extends: diff --git a/.rubocop.yml b/.rubocop.yml index 1d5cf7642c2..27dce2239d8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -56,7 +56,6 @@ Style/FrozenStringLiteralComment: - 'qa/**/*' - 'rubocop/**/*' - 'scripts/**/*' - - 'spec/lib/gitlab/**/*' RSpec/FilePath: Exclude: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9201e691c3f..5c51f879b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 12.5.1 + +### Security (8 changes) + +- Check permissions before showing a forked project's source. +- Encrypt application setting tokens. +- Update Workhorse and Gitaly to fix a security issue. +- Hide commit counts from guest users in Cycle Analytics. +- Limit potential for DNS rebind SSRF in chat notifications. +- Ensure are cleaned by ImportExport::AttributeCleaner. +- Remove notes regarding Related Branches from Issue activity feeds for guest users. +- Escape namespace in label references to prevent XSS. + + ## 12.5.0 ### Security (15 changes) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5e3a4256626..dc87e8af82f 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.73.0 +1.74.0 @@ -264,7 +264,7 @@ gem 'licensee', '~> 8.9' gem 'ace-rails-ap', '~> 4.1.0' # Detect and convert string character encoding -gem 'charlock_holmes', '~> 0.7.7' +gem 'charlock_holmes', '~> 0.7.5' # Detect mime content type from content gem 'mimemagic', '~> 0.3.2' @@ -273,8 +273,8 @@ gem 'mimemagic', '~> 0.3.2' gem 'fast_blank' # Parse time & duration -gem 'chronic', '~> 0.10.2' -gem 'gitlab_chronic_duration', '~> 0.10.6.1' +gem 'gitlab-chronic', '~> 0.10.5' +gem 'gitlab_chronic_duration', '~> 0.10.6.2' gem 'webpack-rails', '~> 0.9.10' gem 'rack-proxy', '~> 0.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5938ec21fd0..ba40ebc6cb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,10 +142,9 @@ GEM mime-types (>= 1.16) cause (0.1) character_set (1.1.2) - charlock_holmes (0.7.7) + charlock_holmes (0.7.6) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) - chronic (0.10.2) chunky_png (1.3.5) citrus (3.0.2) claide (1.0.3) @@ -362,6 +361,8 @@ GEM gitaly (1.73.0) grpc (~> 1.0) github-markup (1.7.0) + gitlab-chronic (0.10.5) + numerizer (~> 0.2) gitlab-labkit (0.7.0) actionpack (>= 5.0.0, < 6.1.0) activesupport (>= 5.0.0, < 6.1.0) @@ -381,8 +382,8 @@ GEM rubocop-gitlab-security (~> 0.1.0) rubocop-performance (~> 1.1.0) rubocop-rspec (~> 1.19) - gitlab_chronic_duration (0.10.6.1) - numerizer (~> 0.1.1) + gitlab_chronic_duration (0.10.6.2) + numerizer (~> 0.2) gitlab_omniauth-ldap (2.1.1) net-ldap (~> 0.16) omniauth (~> 1.3) @@ -630,7 +631,7 @@ GEM notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - numerizer (0.1.1) + numerizer (0.2.0) oauth (0.5.4) oauth2 (1.4.1) faraday (>= 0.8, < 0.16.0) @@ -1143,8 +1144,7 @@ DEPENDENCIES capybara (~> 3.22.0) capybara-screenshot (~> 1.0.22) carrierwave (~> 1.3) - charlock_holmes (~> 0.7.7) - chronic (~> 0.10.2) + charlock_holmes (~> 0.7.5) commonmarker (~> 0.20) concurrent-ruby (~> 1.1) connection_pool (~> 2.0) @@ -1195,6 +1195,7 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly (~> 1.73.0) github-markup (~> 1.7.0) + gitlab-chronic (~> 0.10.5) gitlab-labkit (~> 0.5) gitlab-license (~> 1.0) gitlab-markup (~> 1.7.0) @@ -1202,7 +1203,7 @@ DEPENDENCIES gitlab-peek (~> 0.0.1) gitlab-sidekiq-fetcher (= 0.5.2) gitlab-styles (~> 2.7) - gitlab_chronic_duration (~> 0.10.6.1) + gitlab_chronic_duration (~> 0.10.6.2) gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) google-api-client (~> 0.23) @@ -1 +1 @@ -12.5.0-pre +12.6.0-pre diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index aee9990bc0b..6ec77186298 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,6 +5,8 @@ import { joinPaths } from './lib/utils/url_utility'; import flash from '~/flash'; import { __ } from '~/locale'; +const DEFAULT_PER_PAGE = 20; + const Api = { groupsPath: '/api/:version/groups.json', groupPath: '/api/:version/groups/:id', @@ -66,7 +68,7 @@ const Api = { params: Object.assign( { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, }, options, ), @@ -90,7 +92,7 @@ const Api = { .get(url, { params: { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, }, }) .then(({ data }) => callback(data)); @@ -101,7 +103,7 @@ const Api = { const url = Api.buildUrl(Api.projectsPath); const defaults = { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, simple: true, }; @@ -126,7 +128,7 @@ const Api = { .get(url, { params: { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, ...options, }, }) @@ -235,7 +237,7 @@ const Api = { const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId); const defaults = { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, }; return axios .get(url, { @@ -325,7 +327,7 @@ const Api = { params: Object.assign( { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, }, options, ), @@ -355,7 +357,7 @@ const Api = { const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId); const defaults = { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, }; return axios .get(url, { @@ -371,7 +373,7 @@ const Api = { return axios.get(url, { params: { search: query, - per_page: 20, + per_page: DEFAULT_PER_PAGE, ...options, }, }); @@ -403,10 +405,15 @@ const Api = { return axios.post(url); }, - releases(id) { + releases(id, options = {}) { const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id)); - return axios.get(url); + return axios.get(url, { + params: { + per_page: DEFAULT_PER_PAGE, + ...options, + }, + }); }, release(projectPath, tagName) { diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index eb720f5380b..54d1ee18545 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -14,6 +14,11 @@ export default { GlTooltip: GlTooltipDirective, }, props: { + name: { + type: String, + required: false, + default: '', + }, imageUrl: { type: String, required: true, diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index df74eb2c2f7..2212bfec116 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -4,7 +4,7 @@ import { mapActions, mapState } from 'vuex'; import createFlash from '~/flash'; import { s__, sprintf } from '~/locale'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui'; import createEmptyBadge from '../empty_badge'; import Badge from './badge.vue'; @@ -16,6 +16,8 @@ export default { Badge, LoadingButton, GlLoadingIcon, + GlFormInput, + GlFormGroup, }, props: { isEditing: { @@ -64,6 +66,18 @@ export default { renderedLinkUrl() { return this.renderedBadge ? this.renderedBadge.renderedLinkUrl : ''; }, + name: { + get() { + return this.badge ? this.badge.name : ''; + }, + set(name) { + const badge = this.badge || createEmptyBadge(); + this.updateBadgeInForm({ + ...badge, + name, + }); + }, + }, imageUrl: { get() { return this.badge ? this.badge.imageUrl : ''; @@ -154,6 +168,10 @@ export default { novalidate @submit.prevent.stop="onSubmit" > + <gl-form-group :label="s__('Badges|Name')" label-for="badge-name"> + <gl-form-input id="badge-name" v-model="name" /> + </gl-form-group> + <div class="form-group"> <label for="badge-link-url" class="label-bold">{{ s__('Badges|Link') }}</label> <p v-html="helpText"></p> diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index cad5611c8c5..35d735a2cfd 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -43,13 +43,14 @@ export default { <badge :image-url="badge.renderedImageUrl" :link-url="badge.renderedLinkUrl" - class="table-section section-40" + class="table-section section-30" /> - <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span> - <div class="table-section section-15"> + <div class="table-section section-30"> + <label class="label-bold str-truncated mb-0">{{ badge.name }}</label> <span class="badge badge-pill">{{ badgeKindText }}</span> </div> - <div class="table-section section-15 table-button-footer"> + <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span> + <div class="table-section section-10 table-button-footer"> <div v-if="canEditBadge" class="table-action-buttons"> <button :disabled="badge.isDeleting" diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js index 49a9b5e1be8..527f233bb33 100644 --- a/app/assets/javascripts/badges/empty_badge.js +++ b/app/assets/javascripts/badges/empty_badge.js @@ -1,4 +1,5 @@ export default () => ({ + name: '', imageUrl: '', isDeleting: false, linkUrl: '', diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js index 5542278b3e0..806c2423e7e 100644 --- a/app/assets/javascripts/badges/store/actions.js +++ b/app/assets/javascripts/badges/store/actions.js @@ -1,13 +1,9 @@ import axios from '~/lib/utils/axios_utils'; import types from './mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; export const transformBackendBadge = badge => ({ - id: badge.id, - imageUrl: badge.image_url, - kind: badge.kind, - linkUrl: badge.link_url, - renderedImageUrl: badge.rendered_image_url, - renderedLinkUrl: badge.rendered_link_url, + ...convertObjectPropsToCamelCase(badge, true), isDeleting: false, }); @@ -27,6 +23,7 @@ export default { dispatch('requestNewBadge'); return axios .post(endpoint, { + name: newBadge.name, image_url: newBadge.imageUrl, link_url: newBadge.linkUrl, }) @@ -141,6 +138,7 @@ export default { dispatch('requestUpdatedBadge'); return axios .put(endpoint, { + name: badge.name, image_url: badge.imageUrl, link_url: badge.linkUrl, }) diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index e76e2341dfd..7f69c093902 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -16,7 +16,6 @@ import '~/boards/models/project'; import store from '~/boards/stores'; import boardsStore from '~/boards/stores/boards_store'; import ModalStore from '~/boards/stores/modal_store'; -import BoardService from 'ee_else_ce/boards/services/board_service'; import modalMixin from '~/boards/mixins/modal_mixins'; import '~/boards/filters/due_date_filters'; import Board from 'ee_else_ce/boards/components/board'; @@ -97,7 +96,6 @@ export default () => { bulkUpdatePath: this.bulkUpdatePath, boardId: this.boardId, }); - gl.boardService = new BoardService(); boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); @@ -116,7 +114,7 @@ export default () => { this.filterManager.setup(); boardsStore.disabled = this.disabled; - gl.boardService + boardsStore .all() .then(res => res.data) .then(lists => { @@ -155,7 +153,8 @@ export default () => { newIssue.setFetchingState('subscriptions', true); setWeigthFetchingState(newIssue, true); setEpicFetchingState(newIssue, true); - BoardService.getIssueInfo(sidebarInfoEndpoint) + boardsStore + .getIssueInfo(sidebarInfoEndpoint) .then(res => res.data) .then(data => { const { @@ -211,7 +210,8 @@ export default () => { const { issue } = boardsStore.detail; if (issue.id === id && issue.toggleSubscriptionEndpoint) { issue.setFetchingState('subscriptions', true); - BoardService.toggleIssueSubscription(issue.toggleSubscriptionEndpoint) + boardsStore + .toggleIssueSubscription(issue.toggleSubscriptionEndpoint) .then(() => { issue.setFetchingState('subscriptions', false); issue.updateData({ diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index a951a6bfeea..a0ab20a97aa 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -170,7 +170,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ), ), { - gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/crossplane.html" + gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane" target="_blank" rel="noopener noreferrer"> ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}</a>`, kubectl: `<code>kubectl</code>`, diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index dce9c1a5410..d9805e5e76a 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -311,6 +311,7 @@ export default class CreateMergeRequestDropdown { } onChangeInput(event) { + this.disable(); let target; let value; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 19b85710710..8039a9a7602 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -1,5 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; @@ -36,11 +37,20 @@ export default { GlLoadingIcon, PanelResizer, }, + mixins: [glFeatureFlagsMixin()], props: { endpoint: { type: String, required: true, }, + endpointMetadata: { + type: String, + required: true, + }, + endpointBatch: { + type: String, + required: true, + }, projectPath: { type: String, required: true, @@ -92,6 +102,7 @@ export default { computed: { ...mapState({ isLoading: state => state.diffs.isLoading, + isBatchLoading: state => state.diffs.isBatchLoading, diffFiles: state => state.diffs.diffFiles, diffViewType: state => state.diffs.diffViewType, mergeRequestDiffs: state => state.diffs.mergeRequestDiffs, @@ -153,6 +164,8 @@ export default { mounted() { this.setBaseConfig({ endpoint: this.endpoint, + endpointMetadata: this.endpointMetadata, + endpointBatch: this.endpointBatch, projectPath: this.projectPath, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, @@ -185,6 +198,8 @@ export default { ...mapActions('diffs', [ 'setBaseConfig', 'fetchDiffFiles', + 'fetchDiffFilesMeta', + 'fetchDiffFilesBatch', 'startRenderDiffsQueue', 'assignDiscussionsToDiff', 'setHighlightedRow', @@ -196,24 +211,51 @@ export default { this.assignedDiscussions = false; this.fetchData(false); }, + isLatestVersion() { + return window.location.search.indexOf('diff_id') === -1; + }, fetchData(toggleTree = true) { - this.fetchDiffFiles() - .then(() => { - if (toggleTree) { - this.hideTreeListIfJustOneFile(); - } + if (this.isLatestVersion() && this.glFeatures.diffsBatchLoad) { + this.fetchDiffFilesMeta() + .then(() => { + if (toggleTree) this.hideTreeListIfJustOneFile(); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); - requestIdleCallback( - () => { - this.setDiscussions(); - this.startRenderDiffsQueue(); - }, - { timeout: 1000 }, - ); - }) - .catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); + this.fetchDiffFilesBatch() + .then(() => { + requestIdleCallback( + () => { + this.setDiscussions(); + this.startRenderDiffsQueue(); + }, + { timeout: 1000 }, + ); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); + } else { + this.fetchDiffFiles() + .then(() => { + if (toggleTree) { + this.hideTreeListIfJustOneFile(); + } + + requestIdleCallback( + () => { + this.setDiscussions(); + this.startRenderDiffsQueue(); + }, + { timeout: 1000 }, + ); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); + } if (!this.isNotesFetched) { eventHub.$emit('fetchNotesData'); @@ -324,7 +366,8 @@ export default { }" > <commit-widget v-if="commit" :commit="commit" /> - <template v-if="renderDiffFiles"> + <div v-if="isBatchLoading" class="loading"><gl-loading-icon /></div> + <template v-else-if="renderDiffFiles"> <diff-file v-for="file in diffFiles" :key="file.newPath" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index d84e1af11f3..9de4c38bdf0 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -57,3 +57,4 @@ export const MIN_RENDERING_MS = 2; export const START_RENDERING_INDEX = 200; export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines'; export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines'; +export const DIFFS_PER_PAGE = 10; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index c9580e3d3b4..375ac80021f 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -67,6 +67,8 @@ export default function initDiffsApp(store) { return { endpoint: dataset.endpoint, + endpointMetadata: dataset.endpointMetadata || '', + endpointBatch: dataset.endpointBatch || '', projectPath: dataset.projectPath, helpPagePath: dataset.helpPagePath, currentUser: JSON.parse(dataset.currentUserData) || {}, @@ -100,6 +102,8 @@ export default function initDiffsApp(store) { return createElement('diffs-app', { props: { endpoint: this.endpoint, + endpointMetadata: this.endpointMetadata, + endpointBatch: this.endpointBatch, currentUser: this.currentUser, projectPath: this.projectPath, helpPagePath: this.helpPagePath, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 6695d9fe96c..d4594399ff5 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -13,6 +13,7 @@ import { convertExpandLines, idleCallback, allDiscussionWrappersExpanded, + prepareDiffData, } from './utils'; import * as types from './mutation_types'; import { @@ -33,12 +34,27 @@ import { START_RENDERING_INDEX, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_LINES_KEY, + DIFFS_PER_PAGE, } from '../constants'; import { diffViewerModes } from '~/ide/constants'; export const setBaseConfig = ({ commit }, options) => { - const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options; - commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover }); + const { + endpoint, + endpointMetadata, + endpointBatch, + projectPath, + dismissEndpoint, + showSuggestPopover, + } = options; + commit(types.SET_BASE_CONFIG, { + endpoint, + endpointMetadata, + endpointBatch, + projectPath, + dismissEndpoint, + showSuggestPopover, + }); }; export const fetchDiffFiles = ({ state, commit }) => { @@ -67,6 +83,53 @@ export const fetchDiffFiles = ({ state, commit }) => { .catch(() => worker.terminate()); }; +export const fetchDiffFilesBatch = ({ commit, state }) => { + const baseUrl = `${state.endpointBatch}?per_page=${DIFFS_PER_PAGE}`; + const url = page => (page ? `${baseUrl}&page=${page}` : baseUrl); + + commit(types.SET_BATCH_LOADING, true); + + const getBatch = page => + axios + .get(url(page)) + .then(({ data: { pagination, diff_files } }) => { + commit(types.SET_DIFF_DATA_BATCH, { diff_files }); + commit(types.SET_BATCH_LOADING, false); + return pagination.next_page; + }) + .then(nextPage => nextPage && getBatch(nextPage)); + + return getBatch() + .then(handleLocationHash) + .catch(() => null); +}; + +export const fetchDiffFilesMeta = ({ commit, state }) => { + const worker = new TreeWorker(); + + commit(types.SET_LOADING, true); + + worker.addEventListener('message', ({ data }) => { + commit(types.SET_TREE_DATA, data); + + worker.terminate(); + }); + + return axios + .get(state.endpointMetadata) + .then(({ data }) => { + const strippedData = { ...data }; + strippedData.diff_files = []; + commit(types.SET_LOADING, false); + commit(types.SET_MERGE_REQUEST_DIFFS, data.merge_request_diffs || []); + commit(types.SET_DIFF_DATA, strippedData); + + prepareDiffData(data); + worker.postMessage(data.diff_files); + }) + .catch(() => worker.terminate()); +}; + export const setHighlightedRow = ({ commit }, lineCode) => { const fileHash = lineCode.split('_')[0]; commit(types.SET_HIGHLIGHTED_ROW, lineCode); diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 6821c8445ea..8c52e3178e5 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -8,6 +8,7 @@ const defaultViewType = INLINE_DIFF_VIEW_TYPE; export default () => ({ isLoading: true, + isBatchLoading: false, addedLines: null, removedLines: null, endpoint: '', diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 9db56331faa..5a90d78b2bc 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -1,6 +1,8 @@ export const SET_BASE_CONFIG = 'SET_BASE_CONFIG'; export const SET_LOADING = 'SET_LOADING'; +export const SET_BATCH_LOADING = 'SET_BATCH_LOADING'; export const SET_DIFF_DATA = 'SET_DIFF_DATA'; +export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH'; export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE'; export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS'; export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index a6915a46c00..de2f68d729c 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -12,14 +12,32 @@ import * as types from './mutation_types'; export default { [types.SET_BASE_CONFIG](state, options) { - const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options; - Object.assign(state, { endpoint, projectPath, dismissEndpoint, showSuggestPopover }); + const { + endpoint, + endpointMetadata, + endpointBatch, + projectPath, + dismissEndpoint, + showSuggestPopover, + } = options; + Object.assign(state, { + endpoint, + endpointMetadata, + endpointBatch, + projectPath, + dismissEndpoint, + showSuggestPopover, + }); }, [types.SET_LOADING](state, isLoading) { Object.assign(state, { isLoading }); }, + [types.SET_BATCH_LOADING](state, isBatchLoading) { + Object.assign(state, { isBatchLoading }); + }, + [types.SET_DIFF_DATA](state, data) { prepareDiffData(data); @@ -28,6 +46,12 @@ export default { }); }, + [types.SET_DIFF_DATA_BATCH](state, data) { + prepareDiffData(data); + + state.diffFiles.push(...data.diff_files); + }, + [types.RENDER_FILE](state, file) { Object.assign(file, { renderIt: true, diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index d46bdea9b50..2326018b999 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -252,10 +252,11 @@ export function prepareDiffData(diffData) { showingLines += file.parallel_diff_lines.length; } + const name = (file.viewer && file.viewer.name) || diffViewerModes.text; + Object.assign(file, { renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, - collapsed: - file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED, + collapsed: name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED, isShowingFullFile: false, isLoadingFullFile: false, discussions: [], diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index c94039326aa..dfd4d5474ff 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -6,6 +6,7 @@ import _ from 'underscore'; import { GlTooltipDirective } from '@gitlab/ui'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; @@ -26,7 +27,6 @@ const timeagoInstance = new Timeago(); export default { components: { - UserAvatarLink, CommitComponent, Icon, ActionsComponent, @@ -35,6 +35,8 @@ export default { RollbackComponent, TerminalButtonComponent, MonitoringButtonComponent, + TooltipOnTruncate, + UserAvatarLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -508,12 +510,16 @@ export default { </div> <div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell"> - <a - v-if="shouldRenderBuildName" - :href="buildPath" - class="build-link cgray flex-truncate-parent" - > - <span class="flex-truncate-child">{{ buildName }}</span> + <a v-if="shouldRenderBuildName" :href="buildPath" class="build-link cgray"> + <tooltip-on-truncate + :title="buildName" + truncate-target="child" + class="flex-truncate-parent" + > + <span class="flex-truncate-child"> + {{ buildName }} + </span> + </tooltip-on-truncate> </a> </div> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 81927d18f8b..50c667e6966 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -31,10 +31,6 @@ export default { type: Boolean, required: true, }, - cssContainerClass: { - type: String, - required: true, - }, newEnvironmentPath: { type: String, required: true, @@ -93,7 +89,7 @@ export default { }; </script> <template> - <div :class="cssContainerClass"> + <div> <stop-environment-modal :environment="environmentInStopModal" /> <confirm-rollback-modal :environment="environmentInRollbackModal" /> diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index dcdaf8731f8..9a68619d4f7 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -21,7 +21,6 @@ export default () => newEnvironmentPath: environmentsData.newEnvironmentPath, helpPagePath: environmentsData.helpPagePath, deployBoardsHelpPath: environmentsData.deployBoardsHelpPath, - cssContainerClass: environmentsData.cssClass, canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment), canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), }; @@ -33,7 +32,6 @@ export default () => newEnvironmentPath: this.newEnvironmentPath, helpPagePath: this.helpPagePath, deployBoardsHelpPath: this.deployBoardsHelpPath, - cssContainerClass: this.cssContainerClass, canCreateEnvironment: this.canCreateEnvironment, canReadEnvironment: this.canReadEnvironment, ...this.canaryCalloutProps, diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index ab782f955c6..2000377530b 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -62,6 +62,34 @@ export default { showStacktrace() { return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length); }, + errorTitle() { + return `${this.error.title}`; + }, + errorUrl() { + return sprintf(__('Sentry event: %{external_url}'), { + external_url: this.error.external_url, + }); + }, + errorFirstSeen() { + return sprintf(__('First seen: %{first_seen}'), { first_seen: this.error.first_seen }); + }, + errorLastSeen() { + return sprintf(__('Last seen: %{last_seen}'), { last_seen: this.error.last_seen }); + }, + errorCount() { + return sprintf(__('Events: %{count}'), { count: this.error.count }); + }, + errorUserCount() { + return sprintf(__('Users: %{user_count}'), { user_count: this.error.user_count }); + }, + issueLink() { + return `${this.issueProjectPath}?issue[title]=${encodeURIComponent( + this.errorTitle, + )}&issue[description]=${encodeURIComponent(this.issueDescription)}`; + }, + issueDescription() { + return `${this.errorUrl}${this.errorFirstSeen}${this.errorLastSeen}${this.errorCount}${this.errorUserCount}`; + }, }, mounted() { this.startPollingDetails(this.issueDetailsPath); @@ -86,7 +114,7 @@ export default { <div v-else-if="showDetails" class="error-details"> <div class="top-area align-items-center justify-content-between py-3"> <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span> - <gl-button variant="success" :href="issueProjectPath"> + <gl-button variant="success" :href="issueLink"> {{ __('Create issue') }} </gl-button> </div> diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js index 92ac3a2c94d..78ccef7f253 100644 --- a/app/assets/javascripts/frequent_items/store/mutations.js +++ b/app/assets/javascripts/frequent_items/store/mutations.js @@ -48,7 +48,7 @@ export default { }); }, [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) { - const rawItems = results.data; + const rawItems = results.data ? results.data : results; // Api.groups returns array, Api.projects returns object Object.assign(state, { items: rawItems.map(rawItem => ({ id: rawItem.id, diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 28143859e4c..1ab11892f61 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -602,3 +602,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => { * @return {Number} number of milliseconds */ export const secondsToMilliseconds = seconds => seconds * 1000; + +/** + * Converts the supplied number of seconds to days. + * + * @param {Number} seconds + * @return {Number} number of days + */ +export const secondsToDays = seconds => Math.round(seconds / 86400); + +/** + * Returns the date after the date provided + * + * @param {Date} date the initial date + * @return {Date} the date following the date provided + */ +export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1)); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 2e0270ee42f..cccf9ad311c 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, consistent-return */ +/* eslint-disable func-names, no-param-reassign, operator-assignment, no-else-return, consistent-return */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; @@ -13,8 +13,7 @@ function addBlockTags(blockTag, selected) { } function lineBefore(text, textarea) { - var split; - split = text + const split = text .substring(0, textarea.selectionStart) .trim() .split('\n'); @@ -80,7 +79,7 @@ function moveCursor({ editorSelectionStart, editorSelectionEnd, }) { - var pos; + let pos; if (textArea && !textArea.setSelectionRange) { return; } @@ -132,18 +131,13 @@ export function insertMarkdownText({ select, editor, }) { - var textToInsert, - selectedSplit, - startChar, - removedLastNewLine, - removedFirstNewLine, - currentLineEmpty, - lastNewLine, - editorSelectionStart, - editorSelectionEnd; - removedLastNewLine = false; - removedFirstNewLine = false; - currentLineEmpty = false; + let removedLastNewLine = false; + let removedFirstNewLine = false; + let currentLineEmpty = false; + let editorSelectionStart; + let editorSelectionEnd; + let lastNewLine; + let textToInsert; if (editor) { const selectionRange = editor.getSelectionRange(); @@ -186,7 +180,7 @@ export function insertMarkdownText({ } } - selectedSplit = selected.split('\n'); + const selectedSplit = selected.split('\n'); if (editor && !wrap) { lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row]; @@ -207,8 +201,7 @@ export function insertMarkdownText({ (textArea && textArea.selectionStart === 0) || (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0); - startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; - + const startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; const textPlaceholder = '{text}'; if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { @@ -263,11 +256,10 @@ export function insertMarkdownText({ } function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) { - var $textArea, selected, text; - $textArea = $(textArea); + const $textArea = $(textArea); textArea = $textArea.get(0); - text = $textArea.val(); - selected = selectedText(text, textArea) || tagContent; + const text = $textArea.val(); + const selected = selectedText(text, textArea) || tagContent; $textArea.focus(); return insertMarkdownText({ textArea, diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 0c194d67bce..6bbf118d7d1 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -72,7 +72,7 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3 * @param {String} sha * @returns {String} */ -export const truncateSha = sha => sha.substr(0, 8); +export const truncateSha = sha => sha.substring(0, 8); const ELLIPSIS_CHAR = '…'; export const truncatePathMiddleToLength = (text, maxWidth) => { diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index cafb4b0b479..effcf334cbc 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -126,7 +126,7 @@ export default { /> <gl-dropdown v-gl-tooltip - class="mx-2" + class="ml-auto mx-3" toggle-class="btn btn-transparent border-0" :right="true" :no-caret="true" diff --git a/app/assets/javascripts/monitoring/monitoring_tracking_helper.js b/app/assets/javascripts/monitoring/monitoring_tracking_helper.js new file mode 100644 index 00000000000..5ae1eca10de --- /dev/null +++ b/app/assets/javascripts/monitoring/monitoring_tracking_helper.js @@ -0,0 +1,10 @@ +import Tracking from '~/tracking'; + +const trackDashboardLoad = ({ label, value }) => + Tracking.event(document.body.dataset.page, 'dashboard_fetch', { + label, + property: 'count', + value, + }); + +export default trackDashboardLoad; diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 6a8e3cc82f5..4db720db6e3 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -1,6 +1,7 @@ import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; +import trackDashboardLoad from '../monitoring_tracking_helper'; import statusCodes from '../../lib/utils/http_status'; import { backOff } from '../../lib/utils/common_utils'; import { s__, __ } from '../../locale'; @@ -45,7 +46,7 @@ export const requestMetricsDashboard = ({ commit }) => { export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => { commit(types.SET_ALL_DASHBOARDS, response.all_dashboards); commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups); - dispatch('fetchPrometheusMetrics', params); + return dispatch('fetchPrometheusMetrics', params); }; export const receiveMetricsDashboardFailure = ({ commit }, error) => { commit(types.RECEIVE_METRICS_DATA_FAILURE, error); @@ -83,10 +84,12 @@ export const fetchDashboard = ({ state, dispatch }, params) => { return backOffRequest(() => axios.get(state.dashboardEndpoint, { params })) .then(resp => resp.data) - .then(response => { - dispatch('receiveMetricsDashboardSuccess', { - response, - params, + .then(response => dispatch('receiveMetricsDashboardSuccess', { response, params })) + .then(() => { + const dashboardType = state.currentDashboard === '' ? 'default' : 'custom'; + return trackDashboardLoad({ + label: `${dashboardType}_metrics_dashboard`, + value: state.metricsWithData.length, }); }) .catch(error => { diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 87e94311176..e3300967022 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -1,8 +1,6 @@ import invalidUrl from '~/lib/utils/invalid_url'; export default () => ({ - hasMetrics: false, - showPanels: true, metricsEndpoint: null, environmentsEndpoint: null, deploymentsEndpoint: null, diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 9e4a92426ee..753aa96bb55 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,7 +1,7 @@ <script> -/* global katex */ import marked from 'marked'; import sanitize from 'sanitize-html'; +import katex from 'katex'; import Prompt from './prompt.vue'; const renderer = new marked.Renderer(); @@ -70,7 +70,6 @@ renderer.paragraph = t => { }; marked.setOptions({ - sanitize: true, renderer, }); @@ -87,9 +86,66 @@ export default { computed: { markdown() { return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { - allowedTags: false, + // allowedTags from GitLab's inline HTML guidelines + // https://docs.gitlab.com/ee/user/markdown.html#inline-html + allowedTags: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'h7', + 'h8', + 'br', + 'b', + 'i', + 'strong', + 'em', + 'a', + 'pre', + 'code', + 'img', + 'tt', + 'div', + 'ins', + 'del', + 'sup', + 'sub', + 'p', + 'ol', + 'ul', + 'table', + 'thead', + 'tbody', + 'tfoot', + 'blockquote', + 'dl', + 'dt', + 'dd', + 'kbd', + 'q', + 'samp', + 'var', + 'hr', + 'ruby', + 'rt', + 'rp', + 'li', + 'tr', + 'td', + 'th', + 's', + 'strike', + 'span', + 'abbr', + 'abbr', + 'summary', + ], allowedAttributes: { - '*': ['class'], + '*': ['class', 'style'], + a: ['href'], + img: ['src'], }, }); }, @@ -105,6 +161,15 @@ export default { </template> <style> +/* + Importing the necessary katex stylesheet from the node_module folder rather + than copying the stylesheet into `app/assets/stylesheets/vendors` for + automatic importing via `app/assets/stylesheets/application.scss`. The reason + is that the katex stylesheet depends on many fonts that are in node_module + subfolders - moving all these fonts would make updating katex difficult. + */ +@import '~katex/dist/katex.min.css'; + .markdown .katex { display: block; text-align: center; diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index d7ffa0abb79..98f1f385e9b 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -19,6 +19,7 @@ export default { 'resolvableDiscussionsCount', 'firstUnresolvedDiscussionId', 'unresolvedDiscussionsCount', + 'getDiscussion', ]), isLoggedIn() { return this.getUserData.id; @@ -40,9 +41,10 @@ export default { ...mapActions(['expandDiscussion']), jumpToFirstUnresolvedDiscussion() { const diffTab = window.mrTabs.currentAction === 'diffs'; - const discussionId = this.firstUnresolvedDiscussionId(diffTab); - - this.jumpToDiscussion(discussionId); + const discussionId = + this.firstUnresolvedDiscussionId(diffTab) || this.firstUnresolvedDiscussionId(); + const firstDiscussion = this.getDiscussion(discussionId); + this.jumpToDiscussion(firstDiscussion); }, }, }; diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue index 7fbfe8eebb2..7d742fbfeee 100644 --- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue @@ -19,7 +19,11 @@ export default { }; }, computed: { - ...mapGetters(['nextUnresolvedDiscussionId', 'previousUnresolvedDiscussionId']), + ...mapGetters([ + 'nextUnresolvedDiscussionId', + 'previousUnresolvedDiscussionId', + 'getDiscussion', + ]), }, mounted() { Mousetrap.bind('n', () => this.jumpToNextDiscussion()); @@ -33,14 +37,14 @@ export default { ...mapActions(['expandDiscussion']), jumpToNextDiscussion() { const nextId = this.nextUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView); - - this.jumpToDiscussion(nextId); + const nextDiscussion = this.getDiscussion(nextId); + this.jumpToDiscussion(nextDiscussion); this.currentDiscussionId = nextId; }, jumpToPreviousDiscussion() { const prevId = this.previousUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView); - - this.jumpToDiscussion(prevId); + const prevDiscussion = this.getDiscussion(prevId); + this.jumpToDiscussion(prevDiscussion); this.currentDiscussionId = prevId; }, }, diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 47ec740b63a..62d401d4911 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -84,6 +84,7 @@ export default { 'hasUnresolvedDiscussions', 'showJumpToNextDiscussion', 'getUserData', + 'getDiscussion', ]), currentUser() { return this.getUserData; @@ -221,8 +222,9 @@ export default { this.discussion.id, this.discussionsByDiffOrder, ); + const nextDiscussion = this.getDiscussion(nextId); - this.jumpToDiscussion(nextId); + this.jumpToDiscussion(nextDiscussion); }, deleteNoteHandler(note) { this.$emit('noteDeleted', this.discussion, note); diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index 3d89d907777..94ca01e44cc 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -35,20 +35,26 @@ export default { return false; }, - jumpToDiscussion(id) { + + switchToDiscussionsTabAndJumpTo(id) { + window.mrTabs.eventHub.$once('MergeRequestTabChange', () => { + setTimeout(() => this.discussionJump(id), 0); + }); + + window.mrTabs.tabShown('show'); + }, + + jumpToDiscussion(discussion) { + const { id, diff_discussion: isDiffDiscussion } = discussion; if (id) { const activeTab = window.mrTabs.currentAction; - if (activeTab === 'diffs') { + if (activeTab === 'diffs' && isDiffDiscussion) { this.diffsJump(id); - } else if (activeTab === 'commits' || activeTab === 'pipelines') { - window.mrTabs.eventHub.$once('MergeRequestTabChange', () => { - setTimeout(() => this.discussionJump(id), 0); - }); - - window.mrTabs.tabShown('show'); - } else { + } else if (activeTab === 'show') { this.discussionJump(id); + } else { + this.switchToDiscussionsTabAndJumpTo(id); } } }, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 95466587d6b..16fa6935cbe 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -1,7 +1,7 @@ import { TestStatus } from '~/pipelines/constants'; import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; -function iconForTestStatus(status) { +export function iconForTestStatus(status) { switch (status) { case 'success': return 'status_success_borderless'; diff --git a/app/assets/javascripts/releases/list/components/app.vue b/app/assets/javascripts/releases/list/components/app.vue index 5a06c4fec58..a414b3ccd4e 100644 --- a/app/assets/javascripts/releases/list/components/app.vue +++ b/app/assets/javascripts/releases/list/components/app.vue @@ -1,6 +1,12 @@ <script> import { mapState, mapActions } from 'vuex'; import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui'; +import { + getParameterByName, + historyPushState, + buildUrlWithCurrentLocation, +} from '~/lib/utils/common_utils'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import ReleaseBlock from './release_block.vue'; export default { @@ -9,6 +15,7 @@ export default { GlSkeletonLoading, GlEmptyState, ReleaseBlock, + TablePagination, }, props: { projectId: { @@ -25,7 +32,7 @@ export default { }, }, computed: { - ...mapState(['isLoading', 'releases', 'hasError']), + ...mapState(['isLoading', 'releases', 'hasError', 'pageInfo']), shouldRenderEmptyState() { return !this.releases.length && !this.hasError && !this.isLoading; }, @@ -34,10 +41,17 @@ export default { }, }, created() { - this.fetchReleases(this.projectId); + this.fetchReleases({ + page: getParameterByName('page'), + projectId: this.projectId, + }); }, methods: { ...mapActions(['fetchReleases']), + onChangePage(page) { + historyPushState(buildUrlWithCurrentLocation(`?page=${page}`)); + this.fetchReleases({ page, projectId: this.projectId }); + }, }, }; </script> @@ -67,6 +81,8 @@ export default { :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" /> </div> + + <table-pagination v-if="!isLoading" :change="onChangePage" :page-info="pageInfo" /> </div> </template> <style> diff --git a/app/assets/javascripts/releases/list/components/evidence_block.vue b/app/assets/javascripts/releases/list/components/evidence_block.vue new file mode 100644 index 00000000000..1638a52e73e --- /dev/null +++ b/app/assets/javascripts/releases/list/components/evidence_block.vue @@ -0,0 +1,76 @@ +<script> +import { __, sprintf } from '~/locale'; +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { truncateSha } from '~/lib/utils/text_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ExpandButton from '~/vue_shared/components/expand_button.vue'; + +export default { + name: 'EvidenceBlock', + components: { + ClipboardButton, + ExpandButton, + GlLink, + Icon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + release: { + type: Object, + required: true, + }, + }, + computed: { + evidenceTitle() { + return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tag_name }); + }, + evidenceUrl() { + return this.release.assets && this.release.assets.evidence_file_path; + }, + shortSha() { + return truncateSha(this.sha); + }, + sha() { + return this.release.evidence_sha; + }, + }, +}; +</script> + +<template> + <div> + <div class="card-text prepend-top-default"> + <b> + {{ __('Evidence collection') }} + </b> + </div> + <div class="d-flex align-items-baseline"> + <gl-link + v-gl-tooltip + class="monospace" + :title="__('Download evidence JSON')" + :download="evidenceTitle" + :href="evidenceUrl" + > + <icon name="review-list" class="align-top append-right-4" /><span>{{ evidenceTitle }}</span> + </gl-link> + + <expand-button> + <template slot="short"> + <span class="js-short monospace">{{ shortSha }}</span> + </template> + <template slot="expanded"> + <span class="js-expanded monospace gl-pl-1">{{ sha }}</span> + </template> + </expand-button> + <clipboard-button + :title="__('Copy commit SHA')" + :text="sha" + css-class="btn-default btn-transparent btn-clipboard" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/releases/list/components/release_block.vue b/app/assets/javascripts/releases/list/components/release_block.vue index 2b6aa6aeff9..2ae6b1a595f 100644 --- a/app/assets/javascripts/releases/list/components/release_block.vue +++ b/app/assets/javascripts/releases/list/components/release_block.vue @@ -11,10 +11,12 @@ import { getLocationHash } from '~/lib/utils/url_utility'; import { scrollToElement } from '~/lib/utils/common_utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ReleaseBlockFooter from './release_block_footer.vue'; +import EvidenceBlock from './evidence_block.vue'; export default { name: 'ReleaseBlock', components: { + EvidenceBlock, GlLink, GlBadge, GlButton, @@ -70,6 +72,9 @@ export default { hasAuthor() { return !_.isEmpty(this.author); }, + hasEvidence() { + return Boolean(this.release.evidence_sha); + }, shouldRenderMilestones() { return !_.isEmpty(this.release.milestones); }, @@ -77,9 +82,10 @@ export default { return n__('Milestone', 'Milestones', this.release.milestones.length); }, shouldShowEditButton() { - return Boolean( - this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url, - ); + return Boolean(this.release._links && this.release._links.edit_url); + }, + shouldShowEvidence() { + return this.glFeatures.releaseEvidenceCollection; }, shouldShowFooter() { return this.glFeatures.releaseIssueSummary; @@ -217,6 +223,8 @@ export default { </div> </div> + <evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" /> + <div class="card-text prepend-top-default"> <div v-html="release.description_html"></div> </div> diff --git a/app/assets/javascripts/releases/list/store/actions.js b/app/assets/javascripts/releases/list/store/actions.js index e0a922d5ef6..b15fb69226f 100644 --- a/app/assets/javascripts/releases/list/store/actions.js +++ b/app/assets/javascripts/releases/list/store/actions.js @@ -2,6 +2,7 @@ import * as types from './mutation_types'; import createFlash from '~/flash'; import { __ } from '~/locale'; import api from '~/api'; +import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; /** * Commits a mutation to update the state while the main endpoint is being requested. @@ -16,17 +17,19 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); * * @param {String} projectId */ -export const fetchReleases = ({ dispatch }, projectId) => { +export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => { dispatch('requestReleases'); api - .releases(projectId) - .then(({ data }) => dispatch('receiveReleasesSuccess', data)) + .releases(projectId, { page }) + .then(response => dispatch('receiveReleasesSuccess', response)) .catch(() => dispatch('receiveReleasesError')); }; -export const receiveReleasesSuccess = ({ commit }, data) => - commit(types.RECEIVE_RELEASES_SUCCESS, data); +export const receiveReleasesSuccess = ({ commit }, { data, headers }) => { + const pageInfo = parseIntPagination(normalizeHeaders(headers)); + commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo }); +}; export const receiveReleasesError = ({ commit }) => { commit(types.RECEIVE_RELEASES_ERROR); diff --git a/app/assets/javascripts/releases/list/store/mutations.js b/app/assets/javascripts/releases/list/store/mutations.js index b97dc6cb0ab..99fc096264a 100644 --- a/app/assets/javascripts/releases/list/store/mutations.js +++ b/app/assets/javascripts/releases/list/store/mutations.js @@ -13,13 +13,15 @@ export default { * Sets isLoading to false. * Sets hasError to false. * Sets the received data + * Sets the received pagination information * @param {Object} state - * @param {Object} data + * @param {Object} resp */ - [types.RECEIVE_RELEASES_SUCCESS](state, data) { + [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) { state.hasError = false; state.isLoading = false; state.releases = data; + state.pageInfo = pageInfo; }, /** diff --git a/app/assets/javascripts/releases/list/store/state.js b/app/assets/javascripts/releases/list/store/state.js index bf25e651c99..c251f56c9c5 100644 --- a/app/assets/javascripts/releases/list/store/state.js +++ b/app/assets/javascripts/releases/list/store/state.js @@ -2,4 +2,5 @@ export default () => ({ isLoading: false, hasError: false, releases: [], + pageInfo: {}, }); diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index afb58a60155..f6b9ea5d30d 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -124,7 +124,7 @@ export default { }, { attrs: { - href: this.newBlobPath, + href: `${this.newBlobPath}${this.currentPath}`, class: 'qa-new-file-option', }, text: __('New file'), diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index d826f209815..ae6409a0ac9 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -7,6 +7,7 @@ import TreeActionLink from './components/tree_action_link.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue'; import apolloProvider from './graphql'; import { setTitle } from './utils/title'; +import { updateFormAction } from './utils/dom'; import { parseBoolean } from '../lib/utils/common_utils'; import { webIDEUrl } from '../lib/utils/url_utility'; import { __ } from '../locale'; @@ -42,8 +43,15 @@ export default function setupVueRepositoryList() { forkNewBlobPath, forkNewDirectoryPath, forkUploadBlobPath, + uploadPath, + newDirPath, } = breadcrumbEl.dataset; + router.afterEach(({ params: { pathMatch = '/' } }) => { + updateFormAction('.js-upload-blob-form', uploadPath, pathMatch); + updateFormAction('.js-create-dir-form', newDirPath, pathMatch); + }); + // eslint-disable-next-line no-new new Vue({ el: breadcrumbEl, diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 5bf30e625a0..f97afac85b4 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -7,8 +7,8 @@ import getRef from './queries/getRef.query.graphql'; let fetchpromise; let resolvers = []; -export function resolveCommit(commits, { resolve, entry }) { - const commit = commits.find(c => c.fileName === entry.name && c.type === entry.type); +export function resolveCommit(commits, path, { resolve, entry }) { + const commit = commits.find(c => c.filePath === `${path}/${entry.name}` && c.type === entry.type); if (commit) { resolve(commit); @@ -35,13 +35,13 @@ export function fetchLogsTree(client, path, offset, resolver = null) { .then(({ data, headers }) => { const headerLogsOffset = headers['more-logs-offset']; const { commits } = client.readQuery({ query: getCommits }); - const newCommitData = [...commits, ...normalizeData(data)]; + const newCommitData = [...commits, ...normalizeData(data, path)]; client.writeQuery({ query: getCommits, data: { commits: newCommitData }, }); - resolvers.forEach(r => resolveCommit(newCommitData, r)); + resolvers.forEach(r => resolveCommit(newCommitData, path, r)); fetchpromise = null; diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js index 6c204b57b37..3973798605d 100644 --- a/app/assets/javascripts/repository/utils/commit.js +++ b/app/assets/javascripts/repository/utils/commit.js @@ -1,11 +1,12 @@ // eslint-disable-next-line import/prefer-default-export -export function normalizeData(data, extra = () => {}) { +export function normalizeData(data, path, extra = () => {}) { return data.map(d => ({ sha: d.commit.id, message: d.commit.message, committedDate: d.commit.committed_date, commitPath: d.commit_path, fileName: d.file_name, + filePath: `${path}/${d.file_name}`, type: d.type, __typename: 'LogTreeCommit', ...extra(d), diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js index 963e6fc0bc4..81565a00d82 100644 --- a/app/assets/javascripts/repository/utils/dom.js +++ b/app/assets/javascripts/repository/utils/dom.js @@ -1,4 +1,11 @@ -// eslint-disable-next-line import/prefer-default-export export const updateElementsVisibility = (selector, isVisible) => { document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible)); }; + +export const updateFormAction = (selector, basePath, path) => { + const form = document.querySelector(selector); + + if (form) { + form.action = `${basePath}${path}`; + } +}; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index da1a7c290f8..57fbb88ca2e 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */ +/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */ /* global Issuable */ /* global emitSidebarEvent */ @@ -13,7 +13,7 @@ import { parseBoolean } from './lib/utils/common_utils'; window.emitSidebarEvent = window.emitSidebarEvent || $.noop; function UsersSelect(currentUser, els, options = {}) { - var $els; + const $els = $(els || '.js-user-search'); this.users = this.users.bind(this); this.user = this.user.bind(this); this.usersPath = '/autocomplete/users.json'; @@ -28,36 +28,11 @@ function UsersSelect(currentUser, els, options = {}) { const { handleClick } = options; - $els = $(els); - - if (!els) { - $els = $('.js-user-search'); - } - $els.each( (function(_this) { return function(i, dropdown) { - var options = {}; - var $block, - $collapsedSidebar, - $dropdown, - $loading, - $selectbox, - $value, - abilityName, - assignTo, - assigneeTemplate, - collapsedAssigneeTemplate, - defaultLabel, - defaultNullUser, - firstUser, - issueURL, - selectedId, - selectedIdDefault, - showAnyUser, - showNullUser, - showMenuAbove; - $dropdown = $(dropdown); + const options = {}; + const $dropdown = $(dropdown); options.projectId = $dropdown.data('projectId'); options.groupId = $dropdown.data('groupId'); options.showCurrentUser = $dropdown.data('currentUser'); @@ -65,22 +40,25 @@ function UsersSelect(currentUser, els, options = {}) { options.todoStateFilter = $dropdown.data('todoStateFilter'); options.iid = $dropdown.data('iid'); options.issuableType = $dropdown.data('issuableType'); - showNullUser = $dropdown.data('nullUser'); - defaultNullUser = $dropdown.data('nullUserDefault'); - showMenuAbove = $dropdown.data('showMenuAbove'); - showAnyUser = $dropdown.data('anyUser'); - firstUser = $dropdown.data('firstUser'); + const showNullUser = $dropdown.data('nullUser'); + const defaultNullUser = $dropdown.data('nullUserDefault'); + const showMenuAbove = $dropdown.data('showMenuAbove'); + const showAnyUser = $dropdown.data('anyUser'); + const firstUser = $dropdown.data('firstUser'); options.authorId = $dropdown.data('authorId'); - defaultLabel = $dropdown.data('defaultLabel'); - issueURL = $dropdown.data('issueUpdate'); - $selectbox = $dropdown.closest('.selectbox'); - $block = $selectbox.closest('.block'); - abilityName = $dropdown.data('abilityName'); - $value = $block.find('.value'); - $collapsedSidebar = $block.find('.sidebar-collapsed-user'); - $loading = $block.find('.block-loading').fadeOut(); - selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; - selectedId = $dropdown.data('selected'); + const defaultLabel = $dropdown.data('defaultLabel'); + const issueURL = $dropdown.data('issueUpdate'); + const $selectbox = $dropdown.closest('.selectbox'); + let $block = $selectbox.closest('.block'); + const abilityName = $dropdown.data('abilityName'); + let $value = $block.find('.value'); + const $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + const $loading = $block.find('.block-loading').fadeOut(); + const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; + let selectedId = $dropdown.data('selected'); + let assignTo; + let assigneeTemplate; + let collapsedAssigneeTemplate; if (selectedId === undefined) { selectedId = selectedIdDefault; @@ -207,15 +185,15 @@ function UsersSelect(currentUser, els, options = {}) { }); assignTo = function(selected) { - var data; - data = {}; + const data = {}; data[abilityName] = {}; data[abilityName].assignee_id = selected != null ? selected : null; $loading.removeClass('hidden').fadeIn(); $dropdown.trigger('loading.gl.dropdown'); return axios.put(issueURL, data).then(({ data }) => { - var user, tooltipTitle; + let user = {}; + let tooltipTitle = user.name; $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); if (data.assignee) { @@ -471,10 +449,9 @@ function UsersSelect(currentUser, els, options = {}) { } } - var isIssueIndex, isMRIndex, page, selected; - page = $('body').attr('data-page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = page === page && page === 'projects:merge_requests:index'; + const page = $('body').attr('data-page'); + const isIssueIndex = page === 'projects:issues:index'; + const isMRIndex = page === page && page === 'projects:merge_requests:index'; if ( $dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown') @@ -501,7 +478,7 @@ function UsersSelect(currentUser, els, options = {}) { } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); } else if (!$dropdown.hasClass('js-multiselect')) { - selected = $dropdown + const selected = $dropdown .closest('.selectbox') .find(`input[name='${$dropdown.data('fieldName')}']`) .val(); @@ -544,9 +521,8 @@ function UsersSelect(currentUser, els, options = {}) { }, updateLabel: $dropdown.data('dropdownTitle'), renderRow(user) { - var avatar, img, username; - username = user.username ? `@${user.username}` : ''; - avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; + const username = user.username ? `@${user.username}` : ''; + const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; let selected = false; @@ -565,7 +541,7 @@ function UsersSelect(currentUser, els, options = {}) { selected = user.id === selectedId; } - img = ''; + let img = ''; if (user.beforeDivider != null) { `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape( user.name, @@ -586,35 +562,34 @@ function UsersSelect(currentUser, els, options = {}) { $('.ajax-users-select').each( (function(_this) { return function(i, select) { - var firstUser, showAnyUser, showEmailUser, showNullUser; - var options = {}; + const options = {}; options.skipLdap = $(select).hasClass('skip_ldap'); options.projectId = $(select).data('projectId'); options.groupId = $(select).data('groupId'); options.showCurrentUser = $(select).data('currentUser'); options.authorId = $(select).data('authorId'); options.skipUsers = $(select).data('skipUsers'); - showNullUser = $(select).data('nullUser'); - showAnyUser = $(select).data('anyUser'); - showEmailUser = $(select).data('emailUser'); - firstUser = $(select).data('firstUser'); + const showNullUser = $(select).data('nullUser'); + const showAnyUser = $(select).data('anyUser'); + const showEmailUser = $(select).data('emailUser'); + const firstUser = $(select).data('firstUser'); return $(select).select2({ placeholder: __('Search for a user'), multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query(query) { return _this.users(query.term, options, users => { - var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; - data = { + let name; + const data = { results: users, }; if (query.term.length === 0) { if (firstUser) { // Move current user to the front of the list - ref = data.results; + const ref = data.results; - for (index = 0, len = ref.length; index < len; index += 1) { - obj = ref[index]; + for (let index = 0, len = ref.length; index < len; index += 1) { + const obj = ref[index]; if (obj.username === firstUser) { data.results.splice(index, 1); data.results.unshift(obj); @@ -623,7 +598,7 @@ function UsersSelect(currentUser, els, options = {}) { } } if (showNullUser) { - nullUser = { + const nullUser = { name: s__('UsersSelect|Unassigned'), id: 0, }; @@ -634,7 +609,7 @@ function UsersSelect(currentUser, els, options = {}) { if (name === true) { name = s__('UsersSelect|Any User'); } - anyUser = { + const anyUser = { name, id: null, }; @@ -646,8 +621,8 @@ function UsersSelect(currentUser, els, options = {}) { data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/) ) { - var trimmed = query.term.trim(); - emailUser = { + const trimmed = query.term.trim(); + const emailUser = { name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }), username: trimmed, id: trimmed, @@ -659,18 +634,15 @@ function UsersSelect(currentUser, els, options = {}) { }); }, initSelection() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; return _this.initSelection.apply(_this, args); }, formatResult() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; return _this.formatResult.apply(_this, args); }, formatSelection() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; return _this.formatSelection.apply(_this, args); }, dropdownCssClass: 'ajax-users-dropdown', @@ -687,10 +659,9 @@ function UsersSelect(currentUser, els, options = {}) { } UsersSelect.prototype.initSelection = function(element, callback) { - var id, nullUser; - id = $(element).val(); + const id = $(element).val(); if (id === '0') { - nullUser = { + const nullUser = { name: s__('UsersSelect|Unassigned'), }; return callback(nullUser); @@ -700,11 +671,9 @@ UsersSelect.prototype.initSelection = function(element, callback) { }; UsersSelect.prototype.formatResult = function(user) { - var avatar; + let avatar = gon.default_avatar_url; if (user.avatar_url) { avatar = user.avatar_url; - } else { - avatar = gon.default_avatar_url; } return ` <div class='user-result'> @@ -732,8 +701,7 @@ UsersSelect.prototype.user = function(user_id, callback) { return false; } - var url; - url = this.buildUrl(this.userPath); + let url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); return axios.get(url).then(({ data }) => { callback(data); diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index d64ab774431..e2a6e92081f 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -1,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -15,6 +16,7 @@ import Icon from '~/vue_shared/components/icon.vue'; export default { name: 'ExpandButton', components: { + GlButton, Icon, }, data() { @@ -39,15 +41,25 @@ export default { </script> <template> <span> - <button + <gl-button v-show="isCollapsed" :aria-label="ariaLabel" type="button" - class="text-expander btn-blank" + class="js-text-expander-prepend text-expander btn-blank" @click="onClick" > <icon :size="12" name="ellipsis_h" /> - </button> + </gl-button> + <span v-if="isCollapsed"> <slot name="short"></slot> </span> <span v-if="!isCollapsed"> <slot name="expanded"></slot> </span> + <gl-button + v-show="!isCollapsed" + :aria-label="ariaLabel" + type="button" + class="js-text-expander-append text-expander btn-blank" + @click="onClick" + > + <icon :size="12" name="ellipsis_h" /> + </gl-button> </span> </template> diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1195e467192..5ae4f72de56 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -552,6 +552,11 @@ svg { vertical-align: text-top; } + + a.trial-link gl-emoji { + font-size: $gl-font-size; + vertical-align: baseline; + } } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 2289f0a7011..bd0134a82d3 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -118,7 +118,7 @@ background: none; .select2-search-field input { - padding: 5px $gl-padding / 2; + padding: 5px $gl-input-padding; height: auto; font-family: inherit; font-size: inherit; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 55e4c051a6b..8b2c67378d9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -274,12 +274,6 @@ height: 24px; } - .git-clone-holder { - .btn { - height: auto; - } - } - .dropdown-toggle, .clone-dropdown-btn { .fa { @@ -748,7 +742,7 @@ display: inline-block; &:not(:last-child) { - margin-right: $gl-padding-8; + margin-right: $gl-padding; } &.right { @@ -818,6 +812,10 @@ @extend .btn; @extend .btn-default; } + + .nav > li:not(:last-child) { + margin-right: $gl-padding-8; + } } .repository-languages-bar { diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb index 0c1afdc3d3b..892f6dc657c 100644 --- a/app/controllers/admin/jobs_controller.rb +++ b/app/controllers/admin/jobs_controller.rb @@ -1,25 +1,15 @@ # frozen_string_literal: true class Admin::JobsController < Admin::ApplicationController - # rubocop: disable CodeReuse/ActiveRecord def index + # We need all builds for tabs counters + @all_builds = JobsFinder.new(current_user: current_user).execute + @scope = params[:scope] - @all_builds = Ci::Build - @builds = @all_builds.order('id DESC') - @builds = - case @scope - when 'pending' - @builds.pending.reverse_order - when 'running' - @builds.running.reverse_order - when 'finished' - @builds.finished - else - @builds - end + @builds = JobsFinder.new(current_user: current_user, params: params).execute + @builds = @builds.eager_load_everything @builds = @builds.page(params[:page]).per(30) end - # rubocop: enable CodeReuse/ActiveRecord def cancel_all Ci::Build.running_or_pending.each(&:cancel) diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 673ead04709..9da8ad229fe 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -3,14 +3,14 @@ class Clusters::ClustersController < Clusters::BaseController include RoutableActions - before_action :cluster, only: [:cluster_status, :show, :update, :destroy] + before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache] before_action :generate_gcp_authorize_url, only: [:new] before_action :validate_gcp_token, only: [:new] before_action :gcp_cluster, only: [:new] before_action :user_cluster, only: [:new] before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role, :revoke_aws_role, :aws_proxy] before_action :authorize_update_cluster!, only: [:update] - before_action :authorize_admin_cluster!, only: [:destroy] + before_action :authorize_admin_cluster!, only: [:destroy, :clear_cache] before_action :update_applications_status, only: [:cluster_status] before_action only: [:new, :create_gcp] do push_frontend_feature_flag(:create_eks_clusters) @@ -169,6 +169,12 @@ class Clusters::ClustersController < Clusters::BaseController render json: response.body, status: response.status end + def clear_cache + cluster.delete_cached_resources! + + redirect_to cluster.show_path, notice: _('Cluster cache cleared.') + end + private def destroy_params diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 1645af695be..a78d803927c 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -38,7 +38,8 @@ module CycleAnalyticsParams end def to_utc_time(field) - Date.parse(field).to_time.utc + date = field.is_a?(Date) ? field : Date.parse(field) + date.to_time.utc end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 009765702ab..5cbfabebe39 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -44,7 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action do push_frontend_feature_flag(:vue_issuable_sidebar, project.group) - push_frontend_feature_flag(:release_search_filter, project) + push_frontend_feature_flag(:release_search_filter, project, default_enabled: true) end respond_to :html diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 1d914ab6011..9480900b57a 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -17,34 +17,15 @@ class Projects::JobsController < Projects::ApplicationController layout 'project' - # rubocop: disable CodeReuse/ActiveRecord def index + # We need all builds for tabs counters + @all_builds = JobsFinder.new(current_user: current_user, project: @project).execute + @scope = params[:scope] - @all_builds = project.builds.relevant - @builds = @all_builds.order('ci_builds.id DESC') - @builds = - case @scope - when 'pending' - @builds.pending.reverse_order - when 'running' - @builds.running.reverse_order - when 'finished' - @builds.finished - else - @builds - end - @builds = @builds.includes([ - { pipeline: [:project, :user] }, - :job_artifacts_archive, - :metadata, - :trigger_request, - :project, - :user, - :tags - ]) + @builds = JobsFinder.new(current_user: current_user, project: @project, params: params).execute + @builds = @builds.eager_load_everything @builds = @builds.page(params[:page]).per(30).without_count end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def show diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 766ec1e33f3..566a7ed46ca 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -24,7 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action do push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) - push_frontend_feature_flag(:release_search_filter, @project) + push_frontend_feature_flag(:release_search_filter, @project, default_enabled: true) end around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 39efc3f94dc..a9a264183b3 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -6,8 +6,8 @@ class Projects::ReleasesController < Projects::ApplicationController before_action :release, only: %i[edit update] before_action :authorize_read_release! before_action do - push_frontend_feature_flag(:release_edit_page, project, default_enabled: true) push_frontend_feature_flag(:release_issue_summary, project) + push_frontend_feature_flag(:release_evidence_collection, project) end before_action :authorize_update_release!, only: %i[edit update] @@ -43,7 +43,6 @@ class Projects::ReleasesController < Projects::ApplicationController private def authorize_update_release! - access_denied! unless Feature.enabled?(:release_edit_page, project, default_enabled: true) access_denied! unless can?(current_user, :update_release, release) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index dfddd32d7df..e3ea81d5564 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -13,6 +13,7 @@ # group_id: integer # project_id: integer # milestone_title: string +# release_tag: string # author_id: integer # author_username: string # assignee_id: integer or 'None' or 'Any' @@ -59,6 +60,7 @@ class IssuableFinder author_username label_name milestone_title + release_tag my_reaction_emoji search in @@ -126,6 +128,7 @@ class IssuableFinder items = by_non_archived(items) items = by_iids(items) items = by_milestone(items) + items = by_release(items) items = by_label(items) by_my_reaction_emoji(items) end @@ -364,6 +367,10 @@ class IssuableFinder end end + def releases? + params[:release_tag].present? + end + private def force_cte? @@ -570,6 +577,18 @@ class IssuableFinder end # rubocop: enable CodeReuse/ActiveRecord + def by_release(items) + return items unless releases? + + if filter_by_no_release? + items.without_release + elsif filter_by_any_release? + items.any_release + else + items.with_release(params[:release_tag], params[:project_id]) + end + end + def filter_by_no_milestone? # Accepts `No Milestone` for compatibility params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title @@ -588,6 +607,14 @@ class IssuableFinder params[:milestone_title] == Milestone::Started.name end + def filter_by_no_release? + params[:release_tag].to_s.downcase == FILTER_NONE + end + + def filter_by_any_release? + params[:release_tag].to_s.downcase == FILTER_ANY + end + def by_label(items) return items unless labels? diff --git a/app/finders/jobs_finder.rb b/app/finders/jobs_finder.rb new file mode 100644 index 00000000000..bac18e69618 --- /dev/null +++ b/app/finders/jobs_finder.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class JobsFinder + include Gitlab::Allowable + + def initialize(current_user:, project: nil, params: {}) + @current_user = current_user + @project = project + @params = params + end + + def execute + builds = init_collection.order_id_desc + filter_by_scope(builds) + rescue Gitlab::Access::AccessDeniedError + Ci::Build.none + end + + private + + attr_reader :current_user, :project, :params + + def init_collection + project ? project_builds : all_builds + end + + def all_builds + raise Gitlab::Access::AccessDeniedError unless current_user&.admin? + + Ci::Build.all + end + + def project_builds + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_build, project) + + project.builds.relevant + end + + def filter_by_scope(builds) + case params[:scope] + when 'pending' + builds.pending.reverse_order + when 'running' + builds.running.reverse_order + when 'finished' + builds.finished + else + builds + end + end +end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 1c9c7ec68d0..275a01330bf 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -12,6 +12,7 @@ # group_id: integer # project_id: integer # milestone_title: string +# release_tag: string # author_id: integer # assignee_id: integer # search: string diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index f5aadc42ff0..092a805f275 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -3,7 +3,7 @@ class PipelinesFinder attr_reader :project, :pipelines, :params, :current_user - ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze + ALLOWED_INDEXED_COLUMNS = %w[id status ref updated_at user_id].freeze def initialize(project, current_user, params = {}) @project = project diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 42a15234e57..ac18c17dc61 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -79,7 +79,7 @@ class ProjectsFinder < UnionFinder elsif min_access_level? current_user.authorized_projects(params[:min_access_level]) else - if private_only? + if private_only? || impossible_visibility_level? current_user.authorized_projects else Project.public_or_visible_to_user(current_user) @@ -96,6 +96,30 @@ class ProjectsFinder < UnionFinder end end + # This is an optimization - surprisingly PostgreSQL does not optimize + # for this. + # + # If the default visiblity level and desired visiblity level filter cancels + # each other out, don't use the SQL clause for visibility level in + # `Project.public_or_visible_to_user`. In fact, this then becames equivalent + # to just authorized projects for the user. + # + # E.g. + # (EXISTS(<authorized_projects>) OR projects.visibility_level IN (10,20)) + # AND "projects"."visibility_level" = 0 + # + # is essentially + # EXISTS(<authorized_projects>) AND "projects"."visibility_level" = 0 + # + # See https://gitlab.com/gitlab-org/gitlab/issues/37007 + def impossible_visibility_level? + return unless params[:visibility_level].present? + + public_visibility_levels = Gitlab::VisibilityLevel.levels_for_user(current_user) + + !public_visibility_levels.include?(params[:visibility_level]) + end + def owned_projects? params[:owned].present? end diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb new file mode 100644 index 00000000000..b7fa234a50b --- /dev/null +++ b/app/graphql/mutations/issues/base.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class Base < BaseMutation + include Mutations::ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project the issue to mutate is in" + + argument :iid, GraphQL::STRING_TYPE, + required: true, + description: "The iid of the issue to mutate" + + field :issue, + Types::IssueType, + null: true, + description: "The issue after mutation" + + authorize :update_issue + + private + + def find_object(project_path:, iid:) + project = resolve_project(full_path: project_path) + resolver = Resolvers::IssuesResolver + .single.new(object: project, context: context) + + resolver.resolve(iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb new file mode 100644 index 00000000000..1855c6f053b --- /dev/null +++ b/app/graphql/mutations/issues/set_due_date.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetDueDate < Base + graphql_name 'IssueSetDueDate' + + argument :due_date, + Types::TimeType, + required: true, + description: 'The desired due date for the issue' + + def resolve(project_path:, iid:, due_date:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + ::Issues::UpdateService.new(project, current_user, due_date: due_date) + .execute(issue) + + { + issue: issue, + errors: issue.errors.full_messages + } + end + end + end +end diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb index 932e90c2d22..9fb1249d582 100644 --- a/app/graphql/types/issuable_sort_enum.rb +++ b/app/graphql/types/issuable_sort_enum.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes class IssuableSortEnum < SortEnum graphql_name 'IssuableSort' description 'Values for sorting issuables' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index 4be7260e0b1..c8d8f3ef079 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes class IssueSortEnum < IssuableSortEnum graphql_name 'IssueSort' description 'Values for sorting issues' @@ -10,7 +9,6 @@ module Types value 'DUE_DATE_DESC', 'Due date by descending order', value: 'due_date_desc' value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: 'relative_position_asc' end - # rubocop: enable Graphql/AuthorizeTypes end Types::IssueSortEnum.prepend_if_ee('::EE::Types::IssueSortEnum') diff --git a/app/graphql/types/issue_state_enum.rb b/app/graphql/types/issue_state_enum.rb index 70c34fbe491..6521407fc9d 100644 --- a/app/graphql/types/issue_state_enum.rb +++ b/app/graphql/types/issue_state_enum.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes - # This is a BaseEnum through IssuableEnum, so it does not need authorization class IssueStateEnum < IssuableStateEnum graphql_name 'IssueState' description 'State of a GitLab issue' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb index 37c890a3c8d..92f52726ab3 100644 --- a/app/graphql/types/merge_request_state_enum.rb +++ b/app/graphql/types/merge_request_state_enum.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true module Types - # rubocop: disable Graphql/AuthorizeTypes - # This is a BaseEnum through IssuableEnum, so it does not need authorization class MergeRequestStateEnum < IssuableStateEnum graphql_name 'MergeRequestState' description 'State of a GitLab merge request' value 'merged' end - # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 2408dc7fd1b..ecdbba477d7 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -9,6 +9,7 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::MergeRequests::SetLabels mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetMilestone diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb index 5edc6dcf454..03b71522b84 100644 --- a/app/helpers/git_helper.rb +++ b/app/helpers/git_helper.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true module GitHelper - def strip_gpg_signature(text) + def strip_signature(text) text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") + text.gsub(/-----BEGIN PGP MESSAGE-----(.*)-----END PGP MESSAGE-----/m, "") + text.gsub(/-----BEGIN SIGNED MESSAGE-----(.*)-----END SIGNED MESSAGE-----/m, "") end def short_sha(text) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 3a872622e73..0d3cf4d73fb 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -47,11 +47,11 @@ module LabelsHelper end end - def render_label(label, tooltip: true, link: nil, css: nil) + def render_label(label, tooltip: true, link: nil, css: nil, dataset: nil) # if scoped label is used then EE wraps label tag with scoped label # doc link html = render_colored_label(label, tooltip: tooltip) - html = link_to(html, link, class: css) if link + html = link_to(html, link, class: css, data: dataset) if link html end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index b8f6458b499..899ab70d1aa 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -27,6 +27,16 @@ module MergeRequestsHelper classes.join(' ') end + def state_name_with_icon(merge_request) + if merge_request.merged? + [_("Merged"), "git-merge"] + elsif merge_request.closed? + [_("Closed"), "close"] + else + [_("Open"), "issue-open-m"] + end + end + def ci_build_details_path(merge_request) build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch) return unless build_url diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 64d61bba71a..466c782fc77 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -110,19 +110,26 @@ module ProjectsHelper { project_full_name: project.full_name } end - def remove_fork_project_message(project) - _("You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?") % - { forked_from_project: fork_source_name(project) } - end + def remove_fork_project_description_message(project) + source = visible_fork_source(project) - def fork_source_name(project) - if @project.fork_source - @project.fork_source.full_name + if source + _('This will remove the fork relationship between this project and %{fork_source}.') % + { fork_source: link_to(source.full_name, project_path(source)) } else - @project.fork_network&.deleted_root_project_name + _('This will remove the fork relationship between this project and other projects in the fork network.') end end + def remove_fork_project_warning_message(project) + _("You are going to remove the fork relationship from %{project_full_name}. Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } + end + + def visible_fork_source(project) + project.fork_source if project.fork_source && can?(current_user, :read_project, project.fork_source) + end + def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index fc25b78da93..af1919eeb40 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -158,7 +158,9 @@ module TreeHelper def breadcrumb_data_attributes attrs = { can_collaborate: can_collaborate_with_project?(@project).to_s, - new_blob_path: project_new_blob_path(@project, @id), + new_blob_path: project_new_blob_path(@project, @ref), + upload_path: project_create_blob_path(@project, @ref), + new_dir_path: project_create_dir_path(@project, @ref), new_branch_path: new_project_branch_path(@project), new_tag_path: new_project_tag_path(@project), can_edit_tree: can_edit_tree?.to_s diff --git a/app/models/badge.rb b/app/models/badge.rb index 50299cd6652..eb351425e66 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -22,6 +22,8 @@ class Badge < ApplicationRecord scope :order_created_at_asc, -> { reorder(created_at: :asc) } + scope :with_name, ->(name) { where(name: name) } + validates :link_url, :image_url, addressable_url: true validates :type, presence: true diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 59bff4e2d2b..4679e8b74d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -120,6 +120,20 @@ module Ci scope :eager_load_job_artifacts, -> { includes(:job_artifacts) } + scope :eager_load_everything, -> do + includes( + [ + { pipeline: [:project, :user] }, + :job_artifacts_archive, + :metadata, + :trigger_request, + :project, + :user, + :tags + ] + ) + end + scope :with_exposed_artifacts, -> do joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts) .includes(:metadata, :job_artifacts_metadata) @@ -161,6 +175,7 @@ module Ci end scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) } + scope :order_id_desc, -> { order('ci_builds.id DESC') } acts_as_taggable diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 723bcc8cf3d..b411bc296c5 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -205,14 +205,6 @@ module Ci scope :internal, -> { where(source: internal_sources) } scope :ci_sources, -> { where(config_source: ci_sources_values) } - - scope :sort_by_merge_request_pipelines, -> do - sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' - query = ApplicationRecord.send(:sanitize_sql_array, [sql, sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend - - order(Arel.sql(query)) - end - scope :for_user, -> (user) { where(user: user) } scope :for_sha, -> (sha) { where(sha: sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } @@ -221,22 +213,6 @@ module Ci scope :for_id, -> (id) { where(id: id) } scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } - scope :triggered_by_merge_request, -> (merge_request) do - where(source: :merge_request_event, merge_request: merge_request) - end - - scope :detached_merge_request_pipelines, -> (merge_request, sha) do - triggered_by_merge_request(merge_request).for_sha(sha) - end - - scope :merge_request_pipelines, -> (merge_request, source_sha) do - triggered_by_merge_request(merge_request).for_source_sha(source_sha) - end - - scope :triggered_for_branch, -> (ref) do - where(source: branch_pipeline_sources).where(ref: ref, tag: false) - end - scope :with_reports, -> (reports_scope) do where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1)) end @@ -344,10 +320,6 @@ module Ci sources.reject { |source| source == "external" }.values end - def self.branch_pipeline_sources - @branch_pipeline_sources ||= sources.reject { |source| source == 'merge_request_event' }.values - end - def self.ci_sources_values config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) end @@ -800,18 +772,10 @@ module Ci triggered_by_merge_request? && target_sha.present? end - def merge_train_pipeline? - merge_request_pipeline? && merge_train_ref? - end - def merge_request_ref? MergeRequest.merge_request_ref?(ref) end - def merge_train_ref? - MergeRequest.merge_train_ref?(ref) - end - def matches_sha_or_source_sha?(sha) self.sha == sha || self.source_sha == sha end @@ -844,9 +808,7 @@ module Ci return unless merge_request_event? strong_memoize(:merge_request_event_type) do - if merge_train_pipeline? - :merge_train - elsif merge_request_pipeline? + if merge_request_pipeline? :merged_result elsif detached_merge_request_pipeline? :detached diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 37ba8a7c97e..fd05fd6bab9 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.10.1' + VERSION = '0.11.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 98e754a1370..62b2217a9af 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -267,6 +267,10 @@ module Clusters end end + def delete_cached_resources! + kubernetes_namespaces.delete_all(:delete_all) + end + private def unique_management_project_environment_scope diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index 0e07806dd6f..dde73b567db 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -26,6 +26,7 @@ module Analytics alias_attribute :custom_stage?, :custom scope :default_stages, -> { where(custom: false) } scope :ordered, -> { order(:relative_position, :id) } + scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered } end def parent=(_) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 6ea12e1cd59..205bf4a5a26 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -99,6 +99,8 @@ module Issuable scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :any_milestone, -> { where('milestone_id IS NOT NULL') } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } + scope :any_release, -> { joins_milestone_releases } + scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) } scope :opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) } scope :closed, -> { with_state(:closed) } @@ -120,6 +122,16 @@ module Issuable scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) } scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) } + scope :without_release, -> do + joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id") + .where('milestone_releases.release_id IS NULL') + end + + scope :joins_milestone_releases, -> do + joins("JOIN milestone_releases ON issues.milestone_id = milestone_releases.milestone_id + JOIN releases ON milestone_releases.release_id = releases.id").distinct + end + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :any_label, -> { joins(:label_links).group(:id) } scope :join_project, -> { joins(:project) } @@ -173,7 +185,7 @@ module Issuable private def milestone_is_valid - errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available? + errors.add(:milestone_id, message: "is invalid") if respond_to?(:milestone_id) && milestone_id.present? && !milestone_available? end def description_max_length_for_new_records_is_valid diff --git a/app/models/epic.rb b/app/models/epic.rb index 46723462590..01ef8bd100e 100644 --- a/app/models/epic.rb +++ b/app/models/epic.rb @@ -3,6 +3,8 @@ # Placeholder class for model that is implemented in EE # It reserves '&' as a reference prefix, but the table does not exists in CE class Epic < ApplicationRecord + self.ignored_columns += %i[milestone_id] + def self.link_reference_pattern nil end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a40eaa34bd1..e92042d1056 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1149,26 +1149,6 @@ class MergeRequest < ApplicationRecord actual_head_pipeline.environments end - def state_human_name - if merged? - "Merged" - elsif closed? - "Closed" - else - "Open" - end - end - - def state_icon_name - if merged? - "git-merge" - elsif closed? - "close" - else - "issue-open-m" - end - end - def fetch_ref! target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path) end @@ -1245,16 +1225,8 @@ class MergeRequest < ApplicationRecord end def all_pipelines - return Ci::Pipeline.none unless source_project - - shas = all_commit_shas - strong_memoize(:all_pipelines) do - Ci::Pipeline.from_union( - [source_project.ci_pipelines.merge_request_pipelines(self, shas), - source_project.ci_pipelines.detached_merge_request_pipelines(self, shas), - source_project.ci_pipelines.triggered_for_branch(source_branch).for_sha(shas)], - remove_duplicates: false).sort_by_merge_request_pipelines + MergeRequest::Pipelines.new(self).all end end diff --git a/app/models/merge_request/pipelines.rb b/app/models/merge_request/pipelines.rb new file mode 100644 index 00000000000..cba38f781a6 --- /dev/null +++ b/app/models/merge_request/pipelines.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# A state object to centralize logic related to merge request pipelines +class MergeRequest::Pipelines + include Gitlab::Utils::StrongMemoize + + EVENT = 'merge_request_event' + + def initialize(merge_request) + @merge_request = merge_request + end + + attr_reader :merge_request + + delegate :all_commit_shas, :source_project, :source_branch, to: :merge_request + + def all + return Ci::Pipeline.none unless source_project + + strong_memoize(:all_pipelines) do + pipelines = Ci::Pipeline.from_union( + [source_pipelines, detached_pipelines, triggered_for_branch], + remove_duplicates: false) + + sort(pipelines) + end + end + + private + + def triggered_by_merge_request + source_project.ci_pipelines + .where(source: :merge_request_event, merge_request: merge_request) + end + + def detached_pipelines + triggered_by_merge_request.for_sha(all_commit_shas) + end + + def source_pipelines + triggered_by_merge_request.for_source_sha(all_commit_shas) + end + + def triggered_for_branch + source_project.ci_pipelines + .where(source: branch_pipeline_sources, ref: source_branch, tag: false) + .for_sha(all_commit_shas) + end + + def sources + ::Ci::Pipeline.sources + end + + def branch_pipeline_sources + strong_memoize(:branch_pipeline_sources) do + sources.reject { |source| source == EVENT }.values + end + end + + def sort(pipelines) + sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' + query = ApplicationRecord.send(:sanitize_sql_array, [sql, sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend + + pipelines.order(Arel.sql(query)) + end +end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index a0273fe0e5a..3e0606fd34a 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -22,6 +22,8 @@ class PrometheusService < MonitoringService after_save :clear_reactive_cache! + after_commit :track_events + def initialize_properties if properties.nil? self.properties = {} @@ -116,4 +118,22 @@ class PrometheusService < MonitoringService true end + + def track_events + if enabled_manual_prometheus? + Gitlab::Tracking.event('cluster:services:prometheus', 'enabled_manual_prometheus') + elsif disabled_manual_prometheus? + Gitlab::Tracking.event('cluster:services:prometheus', 'disabled_manual_prometheus') + end + + true + end + + def enabled_manual_prometheus? + manual_configuration_changed? && manual_configuration? + end + + def disabled_manual_prometheus? + manual_configuration_changed? && !manual_configuration? + end end diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index 2306f55f1f4..7677e6f026f 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -65,6 +65,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated raise NotImplementedError end + def clear_cluster_cache_path(cluster) + raise NotImplementedError + end + def cluster_path(cluster, params = {}) raise NotImplementedError end diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb index 54cea19b18e..21db2f6f96b 100644 --- a/app/presenters/group_clusterable_presenter.rb +++ b/app/presenters/group_clusterable_presenter.rb @@ -19,6 +19,11 @@ class GroupClusterablePresenter < ClusterablePresenter update_applications_group_cluster_path(clusterable, cluster, application) end + override :clear_cluster_cache_path + def clear_cluster_cache_path(cluster) + clear_cache_group_cluster_path(clusterable, cluster) + end + override :cluster_path def cluster_path(cluster, params = {}) group_cluster_path(clusterable, cluster, params) diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb index c6572e8ce71..34d3f347689 100644 --- a/app/presenters/instance_clusterable_presenter.rb +++ b/app/presenters/instance_clusterable_presenter.rb @@ -37,6 +37,11 @@ class InstanceClusterablePresenter < ClusterablePresenter update_applications_admin_cluster_path(cluster, application) end + override :clear_cluster_cache_path + def clear_cluster_cache_path(cluster) + clear_cache_admin_cluster_path(cluster) + end + override :cluster_path def cluster_path(cluster, params = {}) admin_cluster_path(cluster, params) diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb index 3fab69fff7a..5c56d42ed27 100644 --- a/app/presenters/project_clusterable_presenter.rb +++ b/app/presenters/project_clusterable_presenter.rb @@ -19,6 +19,11 @@ class ProjectClusterablePresenter < ClusterablePresenter update_applications_project_cluster_path(clusterable, cluster, application) end + override :clear_cluster_cache_path + def clear_cluster_cache_path(cluster) + clear_cache_project_cluster_path(clusterable, cluster) + end + override :cluster_path def cluster_path(cluster, params = {}) project_cluster_path(clusterable, cluster, params) diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index 65034158a06..b38bbc8d96c 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -58,7 +58,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated end def release_edit_page_available? - ::Feature.enabled?(:release_edit_page, project, default_enabled: true) && - can?(current_user, :update_release, release) + can?(current_user, :update_release, release) end end diff --git a/app/serializers/diff_file_metadata_entity.rb b/app/serializers/diff_file_metadata_entity.rb index 500a844b170..05280518f39 100644 --- a/app/serializers/diff_file_metadata_entity.rb +++ b/app/serializers/diff_file_metadata_entity.rb @@ -7,4 +7,7 @@ class DiffFileMetadataEntity < Grape::Entity expose :old_path expose :new_file?, as: :new_file expose :deleted_file?, as: :deleted_file + expose :file_hash do |diff_file| + Digest::SHA1.hexdigest(diff_file.file_path) + end end diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb index 10c89c62bf1..1f5d83917cc 100644 --- a/app/services/issuable/clone/attributes_rewriter.rb +++ b/app/services/issuable/clone/attributes_rewriter.rb @@ -10,7 +10,13 @@ module Issuable end def execute - new_entity.update(milestone: cloneable_milestone, labels: cloneable_labels) + update_attributes = { labels: cloneable_labels } + + milestone = cloneable_milestone + update_attributes[:milestone] = milestone if milestone.present? + + new_entity.update(update_attributes) + copy_resource_label_events end diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 9806090c1a6..cb9f992bb1d 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -58,6 +58,6 @@ = f.text_field :default_ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted = _("The default CI configuration path for new projects.").html_safe - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank' = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 59cdf2016fb..5e34b457231 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -28,6 +28,14 @@ .form-group = field.submit _('Save changes'), class: 'btn btn-success qa-save-domain' + - if @cluster.managed? + .sub-section.form-group + %h4 + = s_('ClusterIntegration|Clear cluster cache') + %p + = s_("ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.") + = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn btn-primary') + .sub-section.form-group %h4.text-danger = s_('ClusterIntegration|Remove Kubernetes cluster integration') diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index d15f0ae3228..88803f982e8 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -20,7 +20,7 @@ = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } - if current_user_menu?(:start_trial) %li - %a.profile-link{ href: trials_link_url } + %a.trial-link{ href: trials_link_url } = s_("CurrentUser|Start a Gold trial") = emoji_icon('rocket') - if current_user_menu?(:settings) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b7c4114d485..daedc52f298 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -74,13 +74,12 @@ - if @project.forked? %p - - if @project.fork_source + - source = visible_fork_source(@project) + - if source #{ s_('ForkedFromProjectPath|Forked from') } - = link_to project_path(@project.fork_source) do - = fork_source_name(@project) + = link_to source.full_name, project_path(source) - else - - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') - = deleted_message % { project_name: fork_source_name(@project) } + = s_('ForkedFromProjectPath|Forked from an inaccessible project') = render_if_exists "projects/home_mirror" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b5e24cbbffb..328fdd0be10 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -126,17 +126,12 @@ - if @project.forked? && can?(current_user, :remove_fork_project, @project) .sub-section %h4.danger-title= _('Remove fork relationship') - %p - = _('This will remove the fork relationship to source project') - = succeed "." do - - if @project.fork_source - = link_to(fork_source_name(@project), project_path(@project.fork_source)) - - else - = fork_source_name(@project) + %p= remove_fork_project_description_message(@project) + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| %p %strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.') - = button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + = button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) } - if can?(current_user, :remove_project, @project) .sub-section diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 92e34b3ceda..552f8dc173a 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -1,5 +1,6 @@ - can_update_merge_request = can?(current_user, :update_merge_request, @merge_request) - can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request) +- state_human_name, state_icon_name = state_name_with_icon(@merge_request) - if @merge_request.closed_without_fork? .alert.alert-danger @@ -8,9 +9,9 @@ .detail-page-header .detail-page-header-body .issuable-status-box.status-box{ class: status_box_class(@merge_request) } - = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'd-block d-sm-none') + = sprite_icon(state_icon_name, size: 16, css_class: 'd-block d-sm-none') %span.d-none.d-sm-block - = @merge_request.state_human_name + = state_human_name .issuable-meta - if @merge_request.discussion_locked? diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index dee6bc8bae4..c2bc5376fd7 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -78,6 +78,8 @@ = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request) #js-diffs-app.diffs.tab-pane{ data: { "is-locked" => @merge_request.discussion_locked?, endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', request.query_parameters), + endpoint_metadata: diffs_metadata_project_json_merge_request_path(@project, @merge_request, 'json', request.query_parameters), + endpoint_batch: diffs_batch_project_json_merge_request_path(@project, @merge_request, 'json', request.query_parameters), help_page_path: suggest_changes_help_path, current_user_data: @current_user_data, project_path: project_path(@merge_request.project), diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index ea815be23c1..a72179f40ad 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -55,7 +55,7 @@ = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted = _("The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>").html_safe - = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' + = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank' %hr .form-group diff --git a/app/views/projects/tags/_tag.atom.builder b/app/views/projects/tags/_tag.atom.builder index 60d4b21b9d1..e4b2428d267 100644 --- a/app/views/projects/tags/_tag.atom.builder +++ b/app/views/projects/tags/_tag.atom.builder @@ -7,7 +7,7 @@ if commit xml.id tag_url xml.link href: tag_url xml.title truncate(tag.name, length: 80) - xml.summary strip_gpg_signature(tag.message) + xml.summary strip_signature(tag.message) xml.content markdown_field(release, :description), type: 'html' xml.updated release.updated_at.xmlschema if release xml.media :thumbnail, width: '40', height: '40', url: image_url(avatar_icon_for_email(commit.author_email)) diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index c7bd0262c54..b04d484c8a7 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -11,7 +11,7 @@ - if tag.message.present? - = strip_gpg_signature(tag.message) + = strip_signature(tag.message) - if commit .block-truncated diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 417cd7a8fee..8086d47479d 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,3 +1,7 @@ +- user = user_email = nil +- if @tag.tagger + - user_email = @tag.tagger.email + - user = User.find_by_any_email(user_email) - add_to_breadcrumbs s_('TagsPage|Tags'), project_tags_path(@project) - breadcrumb_title @tag.name - page_title @tag.name, s_('TagsPage|Tags') @@ -11,6 +15,24 @@ - if protected_tag?(@project, @tag) %span.badge.badge-success = s_('TagsPage|protected') + + - if user + = link_to user_path(user) do + %div + = user_avatar_without_link(user: user, size: 32, css_class: "mt-1 mb-1") + + %div + %strong= user.name + %div= user.to_reference + + - elsif user_email + = mail_to user_email do + %div + = user_avatar_without_link(user_email: user_email, size: 32, css_class: "mt-1 mb-1") + + %div{ :class => "clearfix" } + %strong= user_email + - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else @@ -33,7 +55,7 @@ - if @tag.message.present? %pre.wrap - = strip_gpg_signature(@tag.message) + = strip_signature(@tag.message) .append-bottom-default.prepend-top-default - if @release.description.present? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index d341520e4a2..5da86195243 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -6,7 +6,7 @@ .issues-filters{ class: ("w-100" if type == :boards_modal) } .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class, "v-pre" => type == :boards_modal } - .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0 + .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100 - if type == :boards = render "shared/boards/switcher", board: board = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do @@ -162,8 +162,8 @@ %button.clear-search.hidden{ type: 'button' } = icon('times') .filter-dropdown-container.d-flex.flex-column.flex-md-row - #js-board-labels-toggle - if type == :boards + #js-board-labels-toggle .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } } - if user_can_admin_list = render 'shared/issuable/board_create_list_dropdown', board: board diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 2170b88c7c3..04993f3bc82 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -30,10 +30,10 @@ = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar - milestone = issuable_sidebar[:milestone] || {} - .block.milestone + .block.milestone{ data: { qa_selector: 'milestone_block' } } .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = icon('clock-o', 'aria-hidden': 'true') - %span.milestone-title.collapse-truncated-title + %span.milestone-title.collapse-truncated-title{ data: { qa_selector: 'milestone_title' } } - if milestone.present? = milestone[:title] - else @@ -107,10 +107,10 @@ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } - .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } + .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?), data: { qa_selector: 'labels_block' } } - if selected_labels.any? - selected_labels.each do |label_hash| - = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) + = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'label', qa_label_name: label_hash[:title] }) - else %span.no-value = _('None') diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 67f177288f0..71ec62930d0 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -17,11 +17,11 @@ = render "snippets/actions" .snippet-header.limited-header-width - %h2.snippet-title.prepend-top-0.mb-3.qa-snippet-title + %h2.snippet-title.prepend-top-0.mb-3{ data: { qa_selector: 'snippet_title' } } = markdown_field(@snippet, :title) - if @snippet.description.present? - .description.qa-snippet-description + .description{ data: { qa_selector: 'snippet_description' } } .md = markdown_field(@snippet, :description) %textarea.hidden.js-task-list-field diff --git a/changelogs/unreleased/11403-add-weight-wip-limit-to-list.yml b/changelogs/unreleased/11403-add-weight-wip-limit-to-list.yml new file mode 100644 index 00000000000..18bb11e5ba1 --- /dev/null +++ b/changelogs/unreleased/11403-add-weight-wip-limit-to-list.yml @@ -0,0 +1,5 @@ +--- +title: Add possibility to save max issue weight on lists +merge_request: 19220 +author: +type: added diff --git a/changelogs/unreleased/13768-fix-redo-icn.yml b/changelogs/unreleased/13768-fix-redo-icn.yml index 3ef194bc4b0..ddac33ef6f5 100644 --- a/changelogs/unreleased/13768-fix-redo-icn.yml +++ b/changelogs/unreleased/13768-fix-redo-icn.yml @@ -1,5 +1,5 @@ --- -title: Replacing incorrect icon for Retry in Pipeline list page +title: Replacing incorrect icon in security dashboard. merge_request: 20510 author: type: changed diff --git a/changelogs/unreleased/20668-crossplane-help-link.yml b/changelogs/unreleased/20668-crossplane-help-link.yml new file mode 100644 index 00000000000..dfce6602f5f --- /dev/null +++ b/changelogs/unreleased/20668-crossplane-help-link.yml @@ -0,0 +1,5 @@ +--- +title: Fix Crossplane help link in cluster applications page +merge_request: 20668 +author: +type: fixed diff --git a/changelogs/unreleased/26019-add-evidence-json.yml b/changelogs/unreleased/26019-add-evidence-json.yml new file mode 100644 index 00000000000..91d3754ea58 --- /dev/null +++ b/changelogs/unreleased/26019-add-evidence-json.yml @@ -0,0 +1,5 @@ +--- +title: Add evidence collection for Releases +merge_request: 18874 +author: +type: changed diff --git a/changelogs/unreleased/27630-specify-kubernetes-namespace-in-ci-template.yml b/changelogs/unreleased/27630-specify-kubernetes-namespace-in-ci-template.yml new file mode 100644 index 00000000000..5ec5cb2015d --- /dev/null +++ b/changelogs/unreleased/27630-specify-kubernetes-namespace-in-ci-template.yml @@ -0,0 +1,5 @@ +--- +title: Allow specifying Kubernetes namespace for an environment in gitlab-ci.yml +merge_request: 20270 +author: +type: added diff --git a/changelogs/unreleased/31759-clear-cluster-cache.yml b/changelogs/unreleased/31759-clear-cluster-cache.yml new file mode 100644 index 00000000000..29e0dda43ec --- /dev/null +++ b/changelogs/unreleased/31759-clear-cluster-cache.yml @@ -0,0 +1,5 @@ +--- +title: Add option to delete cached Kubernetes namespaces +merge_request: 20411 +author: +type: added diff --git a/changelogs/unreleased/32557-convert-generic-epic-error-banners-to-form-validation-messages.yml b/changelogs/unreleased/32557-convert-generic-epic-error-banners-to-form-validation-messages.yml new file mode 100644 index 00000000000..d40cfa2fce3 --- /dev/null +++ b/changelogs/unreleased/32557-convert-generic-epic-error-banners-to-form-validation-messages.yml @@ -0,0 +1,5 @@ +--- +title: Convert flash epic error to form validation error +merge_request: 20130 +author: +type: changed diff --git a/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml b/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml new file mode 100644 index 00000000000..0648adc96bf --- /dev/null +++ b/changelogs/unreleased/33099-add-pipelines-api-order-by-updated-at.yml @@ -0,0 +1,5 @@ +--- +title: Allow order_by updated_at in Pipelines API +merge_request: 19886 +author: +type: added diff --git a/changelogs/unreleased/34157-apm_snowplow_events.yml b/changelogs/unreleased/34157-apm_snowplow_events.yml new file mode 100644 index 00000000000..6dfa7ffce5c --- /dev/null +++ b/changelogs/unreleased/34157-apm_snowplow_events.yml @@ -0,0 +1,5 @@ +--- +title: Add snowplow events for APM +merge_request: 19463 +author: +type: added diff --git a/changelogs/unreleased/34157-snowplow-custom-events-for-monitor-apm.yml b/changelogs/unreleased/34157-snowplow-custom-events-for-monitor-apm.yml new file mode 100644 index 00000000000..c90bc071e63 --- /dev/null +++ b/changelogs/unreleased/34157-snowplow-custom-events-for-monitor-apm.yml @@ -0,0 +1,5 @@ +--- +title: Add snowplow events for monitoring dashboard +merge_request: 19455 +author: +type: added diff --git a/changelogs/unreleased/35616-broken-anchor-for-learn-more-about-interacting-with-security-report.yml b/changelogs/unreleased/35616-broken-anchor-for-learn-more-about-interacting-with-security-report.yml new file mode 100644 index 00000000000..6babed09767 --- /dev/null +++ b/changelogs/unreleased/35616-broken-anchor-for-learn-more-about-interacting-with-security-report.yml @@ -0,0 +1,5 @@ +--- +title: Use correct fragment identifier for vulnerability help path +merge_request: 20524 +author: +type: fixed diff --git a/changelogs/unreleased/36313-graphql-mutation-for-changing-due-date-of-an-issue.yml b/changelogs/unreleased/36313-graphql-mutation-for-changing-due-date-of-an-issue.yml new file mode 100644 index 00000000000..f63605bd593 --- /dev/null +++ b/changelogs/unreleased/36313-graphql-mutation-for-changing-due-date-of-an-issue.yml @@ -0,0 +1,5 @@ +--- +title: Add GraphQL mutation for changing due date of an issue +merge_request: 20577 +author: +type: added diff --git a/changelogs/unreleased/36611-gitlab-container-registry-repository-names-regex-is-not-at-parity-w.yml b/changelogs/unreleased/36611-gitlab-container-registry-repository-names-regex-is-not-at-parity-w.yml new file mode 100644 index 00000000000..b8f2c402d90 --- /dev/null +++ b/changelogs/unreleased/36611-gitlab-container-registry-repository-names-regex-is-not-at-parity-w.yml @@ -0,0 +1,5 @@ +--- +title: Update Container Registry naming restrictions to allow for sequential '-' +merge_request: 20318 +author: +type: fixed diff --git a/changelogs/unreleased/37000-reduce-start-a-trial-emoji-rocket-size.yml b/changelogs/unreleased/37000-reduce-start-a-trial-emoji-rocket-size.yml new file mode 100644 index 00000000000..5a30c7fa431 --- /dev/null +++ b/changelogs/unreleased/37000-reduce-start-a-trial-emoji-rocket-size.yml @@ -0,0 +1,5 @@ +--- +title: Reduce start a trial rocket emoji size +merge_request: 20579 +author: +type: changed diff --git a/changelogs/unreleased/37033-auto-devops-suppress-progress-on-pulling-docker-base-image-to-be-ru.yml b/changelogs/unreleased/37033-auto-devops-suppress-progress-on-pulling-docker-base-image-to-be-ru.yml new file mode 100644 index 00000000000..dbaa3ec44be --- /dev/null +++ b/changelogs/unreleased/37033-auto-devops-suppress-progress-on-pulling-docker-base-image-to-be-ru.yml @@ -0,0 +1,5 @@ +--- +title: Suppress progress on pulling image on Code Quality of Auto DevOps +merge_request: 20604 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/37034-increase-margin-between-commits-branches-tags-size-row-in-project-d.yml b/changelogs/unreleased/37034-increase-margin-between-commits-branches-tags-size-row-in-project-d.yml new file mode 100644 index 00000000000..75a64974725 --- /dev/null +++ b/changelogs/unreleased/37034-increase-margin-between-commits-branches-tags-size-row-in-project-d.yml @@ -0,0 +1,5 @@ +--- +title: Increase margin between project stats +merge_request: 20606 +author: +type: other diff --git a/changelogs/unreleased/7150-the-weight-assignment-box-shows-up-when-the-sidebar-is-collapsed-eve.yml b/changelogs/unreleased/7150-the-weight-assignment-box-shows-up-when-the-sidebar-is-collapsed-eve.yml new file mode 100644 index 00000000000..9f8de281e77 --- /dev/null +++ b/changelogs/unreleased/7150-the-weight-assignment-box-shows-up-when-the-sidebar-is-collapsed-eve.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue trying to edit weight with collapsed sidebar as guest +merge_request: 20431 +author: +type: fixed diff --git a/changelogs/unreleased/Upate-boards-index-js-to-use-boardsStore.yml b/changelogs/unreleased/Upate-boards-index-js-to-use-boardsStore.yml new file mode 100644 index 00000000000..bccf259f312 --- /dev/null +++ b/changelogs/unreleased/Upate-boards-index-js-to-use-boardsStore.yml @@ -0,0 +1,5 @@ +--- +title: Removed all references of BoardService +merge_request: 20144 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/add-badge-name-field.yml b/changelogs/unreleased/add-badge-name-field.yml new file mode 100644 index 00000000000..59592167c89 --- /dev/null +++ b/changelogs/unreleased/add-badge-name-field.yml @@ -0,0 +1,5 @@ +--- +title: Add badge name field +merge_request: 16998 +author: Lee Tickett +type: added diff --git a/changelogs/unreleased/bvl-remove-cleanup-feature-flag.yml b/changelogs/unreleased/bvl-remove-cleanup-feature-flag.yml new file mode 100644 index 00000000000..31ec5157b1f --- /dev/null +++ b/changelogs/unreleased/bvl-remove-cleanup-feature-flag.yml @@ -0,0 +1,6 @@ +--- +title: Try longer to clean up after using a gpg-keychain and raise exption if the + cleanup fails +merge_request: 20718 +author: +type: fixed diff --git a/changelogs/unreleased/env-tooltips.yml b/changelogs/unreleased/env-tooltips.yml new file mode 100644 index 00000000000..f2d33bea1e5 --- /dev/null +++ b/changelogs/unreleased/env-tooltips.yml @@ -0,0 +1,5 @@ +--- +title: Fix tooltip hovers in environments table +merge_request: 20737 +author: +type: fixed diff --git a/changelogs/unreleased/feat-tagger.yml b/changelogs/unreleased/feat-tagger.yml new file mode 100644 index 00000000000..e217c2e1be9 --- /dev/null +++ b/changelogs/unreleased/feat-tagger.yml @@ -0,0 +1,5 @@ +--- +title: add tagger within tag view +merge_request: 19681 +author: Roger Meier +type: added diff --git a/changelogs/unreleased/feat-ui-releases-pagination.yml b/changelogs/unreleased/feat-ui-releases-pagination.yml new file mode 100644 index 00000000000..8f6efe8ca01 --- /dev/null +++ b/changelogs/unreleased/feat-ui-releases-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Implement pagination for project releases page +merge_request: 19912 +author: Fabio Huser +type: added diff --git a/changelogs/unreleased/fix-groups-search-dropdown.yml b/changelogs/unreleased/fix-groups-search-dropdown.yml new file mode 100644 index 00000000000..7dd1a5a1d4f --- /dev/null +++ b/changelogs/unreleased/fix-groups-search-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix group search in groups dropdown +merge_request: 20535 +author: +type: fixed diff --git a/changelogs/unreleased/fix-incorrect-new-branch-name-from-issue.yml b/changelogs/unreleased/fix-incorrect-new-branch-name-from-issue.yml new file mode 100644 index 00000000000..1afd61e40d0 --- /dev/null +++ b/changelogs/unreleased/fix-incorrect-new-branch-name-from-issue.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect new branch name from issue +merge_request: 20677 +author: Lee Tickett +type: fixed diff --git a/changelogs/unreleased/fix-padding-in-project-settings-members.yml b/changelogs/unreleased/fix-padding-in-project-settings-members.yml new file mode 100644 index 00000000000..f77821ce83a --- /dev/null +++ b/changelogs/unreleased/fix-padding-in-project-settings-members.yml @@ -0,0 +1,5 @@ +--- +title: Fix multi select input padding in project and group user select +merge_request: 20520 +author: Kevin Lee +type: fixed diff --git a/changelogs/unreleased/gitaly-version-v1.74.0.yml b/changelogs/unreleased/gitaly-version-v1.74.0.yml new file mode 100644 index 00000000000..bad7ed8fddb --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.74.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.74.0 +merge_request: 20706 +author: +type: changed diff --git a/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml b/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml new file mode 100644 index 00000000000..a5881b6e187 --- /dev/null +++ b/changelogs/unreleased/include-worker-attributes-in-sidekiq-metrics-v2.yml @@ -0,0 +1,5 @@ +--- +title: Add worker attributes to Sidekiq metrics +merge_request: 20292 +author: +type: other diff --git a/changelogs/unreleased/jivanvl-fix-alignment-options-dropdown-graph.yml b/changelogs/unreleased/jivanvl-fix-alignment-options-dropdown-graph.yml new file mode 100644 index 00000000000..cff03023b4c --- /dev/null +++ b/changelogs/unreleased/jivanvl-fix-alignment-options-dropdown-graph.yml @@ -0,0 +1,5 @@ +--- +title: Fix dropdown location on the monitoring charts +merge_request: 20400 +author: +type: fixed diff --git a/changelogs/unreleased/new-33257-prevent-accidental-deletions-via-soft-delete-for-groups-db-chan.yml b/changelogs/unreleased/new-33257-prevent-accidental-deletions-via-soft-delete-for-groups-db-chan.yml new file mode 100644 index 00000000000..6b300dd53f1 --- /dev/null +++ b/changelogs/unreleased/new-33257-prevent-accidental-deletions-via-soft-delete-for-groups-db-chan.yml @@ -0,0 +1,5 @@ +--- +title: Add migrations for 'soft-delete for groups' feature +merge_request: 20276 +author: +type: added diff --git a/changelogs/unreleased/projects_finder_visibility_optimization.yml b/changelogs/unreleased/projects_finder_visibility_optimization.yml new file mode 100644 index 00000000000..9d6d626d5cb --- /dev/null +++ b/changelogs/unreleased/projects_finder_visibility_optimization.yml @@ -0,0 +1,5 @@ +--- +title: Optimize query when Projects API requests private visibility level +merge_request: 20594 +author: +type: performance diff --git a/changelogs/unreleased/remove_milestone_id_from_epics.yml b/changelogs/unreleased/remove_milestone_id_from_epics.yml new file mode 100644 index 00000000000..f124a01c9dd --- /dev/null +++ b/changelogs/unreleased/remove_milestone_id_from_epics.yml @@ -0,0 +1,5 @@ +--- +title: Remove milestone_id from epics +merge_request: 20187 +author: Lee Tickett +type: other diff --git a/changelogs/unreleased/security-28802-respect-fork-parent-visibility-ee.yml b/changelogs/unreleased/security-28802-respect-fork-parent-visibility-ee.yml new file mode 100644 index 00000000000..8872b73a0cc --- /dev/null +++ b/changelogs/unreleased/security-28802-respect-fork-parent-visibility-ee.yml @@ -0,0 +1,5 @@ +--- +title: Check permissions before showing a forked project's source +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-exclude_ids_attribute_cleaning.yml b/changelogs/unreleased/security-exclude_ids_attribute_cleaning.yml new file mode 100644 index 00000000000..08fc1393f20 --- /dev/null +++ b/changelogs/unreleased/security-exclude_ids_attribute_cleaning.yml @@ -0,0 +1,5 @@ +--- +title: Ensure are cleaned by ImportExport::AttributeCleaner +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-api-project-template-creation.yml b/changelogs/unreleased/sh-fix-api-project-template-creation.yml new file mode 100644 index 00000000000..787bd147c91 --- /dev/null +++ b/changelogs/unreleased/sh-fix-api-project-template-creation.yml @@ -0,0 +1,5 @@ +--- +title: Fix project creation with templates using /projects/user/:id API +merge_request: 20590 +author: +type: fixed diff --git a/changelogs/unreleased/sh-upgrade-gitlab-chronic.yml b/changelogs/unreleased/sh-upgrade-gitlab-chronic.yml new file mode 100644 index 00000000000..c66b73fefa0 --- /dev/null +++ b/changelogs/unreleased/sh-upgrade-gitlab-chronic.yml @@ -0,0 +1,5 @@ +--- +title: Fix cron parsing for Daylight Savings +merge_request: 20667 +author: +type: fixed diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-11-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-11-0.yml new file mode 100644 index 00000000000..a6a22b8f55f --- /dev/null +++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-11-0.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Runner Helm Chart to 0.11.0 +merge_request: 20461 +author: +type: other diff --git a/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml b/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml new file mode 100644 index 00000000000..4dedf4e8d1d --- /dev/null +++ b/changelogs/unreleased/xanf-fix-unresolved-discussion-jump.yml @@ -0,0 +1,5 @@ +--- +title: Ensure next unresolved discussion button takes user to the right place +merge_request: 20620 +author: +type: fixed diff --git a/config/pseudonymizer.yml b/config/pseudonymizer.yml index 1c06366c237..1f4ed9a8421 100644 --- a/config/pseudonymizer.yml +++ b/config/pseudonymizer.yml @@ -47,7 +47,6 @@ tables: epics: whitelist: - id - - milestone_id - group_id - author_id - assignee_id diff --git a/config/routes.rb b/config/routes.rb index 9fb4d94f068..c98c0358336 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -166,6 +166,7 @@ Rails.application.routes.draw do end get :cluster_status, format: :json + delete :clear_cache end end end diff --git a/db/migrate/20190606202100_add_name_to_badges.rb b/db/migrate/20190606202100_add_name_to_badges.rb new file mode 100644 index 00000000000..472e1202ad8 --- /dev/null +++ b/db/migrate/20190606202100_add_name_to_badges.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddNameToBadges < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :badges, :name, :string, null: true, limit: 255 + end +end diff --git a/db/migrate/20191028130054_add_max_issue_weight_to_list.rb b/db/migrate/20191028130054_add_max_issue_weight_to_list.rb new file mode 100644 index 00000000000..eec7c42c907 --- /dev/null +++ b/db/migrate/20191028130054_add_max_issue_weight_to_list.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddMaxIssueWeightToList < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default :lists, :max_issue_weight, :integer, default: 0 + end + + def down + remove_column :lists, :max_issue_weight + end +end diff --git a/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb b/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb new file mode 100644 index 00000000000..566bb16ac65 --- /dev/null +++ b/db/migrate/20191111175230_add_index_on_ci_pipelines_updated_at.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexOnCiPipelinesUpdatedAt < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_COLUMNS = [:project_id, :status, :updated_at] + + disable_ddl_transaction! + + def up + add_concurrent_index(:ci_pipelines, INDEX_COLUMNS) + end + + def down + remove_concurrent_index(:ci_pipelines, INDEX_COLUMNS) + end +end diff --git a/db/migrate/20191118053631_add_group_deletion_schedules.rb b/db/migrate/20191118053631_add_group_deletion_schedules.rb new file mode 100644 index 00000000000..6f3ed27e156 --- /dev/null +++ b/db/migrate/20191118053631_add_group_deletion_schedules.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class AddGroupDeletionSchedules < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + create_table :group_deletion_schedules, id: false do |t| + t.references :group, + foreign_key: { on_delete: :cascade, to_table: :namespaces }, + default: nil, + index: false, + primary_key: true + + t.references :user, + index: true, + foreign_key: { on_delete: :nullify }, + null: false + + t.date :marked_for_deletion_on, + index: true, + null: false + end + end + + def down + drop_table :group_deletion_schedules + end +end diff --git a/db/migrate/20191124150431_change_label_id_index_to_include_action_on_label_events.rb b/db/migrate/20191124150431_change_label_id_index_to_include_action_on_label_events.rb new file mode 100644 index 00000000000..bd138adc5fa --- /dev/null +++ b/db/migrate/20191124150431_change_label_id_index_to_include_action_on_label_events.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ChangeLabelIdIndexToIncludeActionOnLabelEvents < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:resource_label_events, %I[label_id action]) + + remove_concurrent_index(:resource_label_events, :label_id) + end + + def down + add_concurrent_index(:resource_label_events, :label_id) + + remove_concurrent_index(:resource_label_events, %I[label_id action]) + end +end diff --git a/db/schema.rb b/db/schema.rb index 8777e6394d8..9dccceb79f0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_11_19_023952) do +ActiveRecord::Schema.define(version: 2019_11_24_150431) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -498,6 +498,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do t.integer "project_id" t.integer "group_id" t.string "type", null: false + t.string "name", limit: 255 t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false t.index ["group_id"], name: "index_badges_on_group_id" @@ -850,6 +851,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do t.index ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha" t.index ["project_id", "source"], name: "index_ci_pipelines_on_project_id_and_source" t.index ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source" + t.index ["project_id", "status", "updated_at"], name: "index_ci_pipelines_on_project_id_and_status_and_updated_at" t.index ["project_id"], name: "index_ci_pipelines_on_project_id" t.index ["status"], name: "index_ci_pipelines_on_status" t.index ["user_id"], name: "index_ci_pipelines_on_user_id" @@ -1890,6 +1892,13 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do t.index ["key", "value"], name: "index_group_custom_attributes_on_key_and_value" end + create_table "group_deletion_schedules", primary_key: "group_id", id: :bigint, default: nil, force: :cascade do |t| + t.bigint "user_id", null: false + t.date "marked_for_deletion_on", null: false + t.index ["marked_for_deletion_on"], name: "index_group_deletion_schedules_on_marked_for_deletion_on" + t.index ["user_id"], name: "index_group_deletion_schedules_on_user_id" + end + create_table "group_group_links", force: :cascade do |t| t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false @@ -2249,6 +2258,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do t.integer "user_id" t.integer "milestone_id" t.integer "max_issue_count", default: 0, null: false + t.integer "max_issue_weight", default: 0, null: false t.index ["board_id", "label_id"], name: "index_lists_on_board_id_and_label_id", unique: true t.index ["label_id"], name: "index_lists_on_label_id" t.index ["list_type"], name: "index_lists_on_list_type" @@ -3467,7 +3477,7 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do t.text "reference_html" t.index ["epic_id"], name: "index_resource_label_events_on_epic_id" t.index ["issue_id"], name: "index_resource_label_events_on_issue_id" - t.index ["label_id"], name: "index_resource_label_events_on_label_id" + t.index ["label_id", "action"], name: "index_resource_label_events_on_label_id_and_action" t.index ["merge_request_id"], name: "index_resource_label_events_on_merge_request_id" t.index ["user_id"], name: "index_resource_label_events_on_user_id" end @@ -4410,6 +4420,8 @@ ActiveRecord::Schema.define(version: 2019_11_19_023952) do add_foreign_key "gpg_signatures", "projects", on_delete: :cascade add_foreign_key "grafana_integrations", "projects", on_delete: :cascade add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "group_deletion_schedules", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "group_deletion_schedules", "users", on_delete: :nullify add_foreign_key "group_group_links", "namespaces", column: "shared_group_id", on_delete: :cascade add_foreign_key "group_group_links", "namespaces", column: "shared_with_group_id", on_delete: :cascade add_foreign_key "identities", "saml_providers", name: "fk_aade90f0fc", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index f5bef4a4ad4..fb343ae91bb 100644 --- a/doc/README.md +++ b/doc/README.md @@ -23,7 +23,7 @@ No matter how you use GitLab, we have documentation for you. | Essential Documentation | Essential Documentation | |:-------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------| | [**User Documentation**](user/index.md)<br/>Discover features and concepts for GitLab users. | [**Administrator documentation**](administration/index.md)<br/>Everything GitLab self-managed administrators need to know. | -| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have resources to get you started. | +| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have the resources to get you started. | | [**Building an integration with GitLab?**](#building-an-integration-with-gitlab)<br/>Consult our automation and integration documentation. | [**Coming to GitLab from another platform?**](#coming-to-gitlab-from-another-platform)<br/>Consult our handy guides. | | [**Install GitLab**](https://about.gitlab.com/install/)<br/>Installation options for different platforms. | [**Customers**](subscriptions/index.md)<br/>Information for new and existing customers. | | [**Update GitLab**](update/README.md)<br/>Update your GitLab self-managed instance to the latest version. | [**GitLab Releases**](https://about.gitlab.com/releases/)<br/>What's new in GitLab. | @@ -42,6 +42,7 @@ Have a look at some of our most popular documentation resources: | [Kubernetes integration](user/project/clusters/index.md) | Use GitLab with Kubernetes. | | [SSH authentication](ssh/README.md) | Secure your network communications. | | [Using Docker images](ci/docker/using_docker_images.md) | Build and test your applications with Docker. | +| [GraphQL](api/graphql/index.md) | Explore GitLab's GraphQL API. | ## The entire DevOps Lifecycle @@ -411,7 +412,7 @@ Learn more about using Git, and using Git with GitLab: | Topic | Description | |:----------------------------------------------------------------------------|:---------------------------------------------------------------------------| | [Git](topics/git/index.md) | Getting started with Git, branching strategies, Git LFS, and advanced use. | -| [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF describing the most used Git operations. | +| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF describing the most used Git operations. | | [GitLab Flow](topics/gitlab_flow.md) | Explore the best of Git with the GitLab Flow strategy. | <div align="right"> diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md index 0b5ddfd03ee..e15f91ebab2 100644 --- a/doc/administration/operations/extra_sidekiq_processes.md +++ b/doc/administration/operations/extra_sidekiq_processes.md @@ -126,12 +126,26 @@ queues will use three threads in total. ## Limiting concurrency -To limit the concurrency of the Sidekiq processes: +To limit the concurrency of the Sidekiq process: 1. Edit `/etc/gitlab/gitlab.rb` and add: ```ruby - sidekiq_cluster['concurrency'] = 25 + sidekiq['concurrency'] = 25 + ``` + +1. Save the file and reconfigure GitLab for the changes to take effect: + + ```sh + sudo gitlab-ctl reconfigure + ``` + +To limit the max concurrency of the Sidekiq cluster processes: + +1. Edit `/etc/gitlab/gitlab.rb` and add: + + ```ruby + sidekiq_cluster['max_concurrency'] = 25 ``` 1. Save the file and reconfigure GitLab for the changes to take effect: diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index dd220d0871d..ca58c4f6836 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -258,6 +258,22 @@ Project.find_each do |project| end ``` +## Wikis + +### Recreate + +A Projects Wiki can be recreated by + +**Note:** This is a destructive operation, the Wiki will be empty + +```ruby +p = Project.find_by_full_path('<username-or-group>/<project-name>') ### enter your projects path + +GitlabShellWorker.perform_in(0, :remove_repository, p.repository_storage, p.wiki.disk_path) ### deletes the wiki project from the filesystem + +p.create_wiki ### creates the wiki project on the filesystem +``` + ## Imports / Exports ```ruby diff --git a/doc/api/boards.md b/doc/api/boards.md index b9c2a984dc5..9ebe1570a59 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -49,7 +49,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 2, @@ -59,7 +60,8 @@ Example response: "description" : null }, "position" : 2, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 3, @@ -69,7 +71,8 @@ Example response: "description" : null }, "position" : 3, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ] } @@ -121,7 +124,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 2, @@ -131,7 +135,8 @@ Example response: "description" : null }, "position" : 2, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 3, @@ -141,7 +146,8 @@ Example response: "description" : null }, "position" : 3, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ] } @@ -192,7 +198,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 2, @@ -202,7 +209,8 @@ Example response: "description" : null }, "position" : 2, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 3, @@ -212,7 +220,8 @@ Example response: "description" : null }, "position" : 3, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ] } @@ -346,7 +355,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 2, @@ -356,7 +366,8 @@ Example response: "description" : null }, "position" : 2, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 }, { "id" : 3, @@ -366,7 +377,8 @@ Example response: "description" : null }, "position" : 3, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ] ``` @@ -400,7 +412,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ``` @@ -441,7 +454,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ``` @@ -475,7 +489,8 @@ Example response: "description" : null }, "position" : 1, - "max_issue_count": 0 + "max_issue_count": 0, + "max_issue_weight": 0 } ``` diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md new file mode 100644 index 00000000000..aab8c26ae99 --- /dev/null +++ b/doc/api/graphql/getting_started.md @@ -0,0 +1,386 @@ +# Getting started with GitLab GraphQL API + +This guide demonstrates basic usage of GitLab's GraphQL API. + +See the [GraphQL API StyleGuide](../../development/api_graphql_styleguide.md) for implementation details +aimed at developers who wish to work on developing the API itself. + +## Running examples + +The examples documented here can be run using: + +- The command line. +- GraphiQL. + +### Command line + +You can run GraphQL queries in a `curl` request on the command line on your local machine. +A GraphQL request can be made as a `POST` request to `/api/graphql` with the query as the payload. +You can authorize your request by generating a [personal access token](../../user/profile/personal_access_tokens.md) +to use as a bearer token. + +Example: + +```sh +GRAPHQL_TOKEN = <your-token> +curl 'http://gitlab.com/api/graphql' --header "Authorization: Bearer $GRAPHQL_TOKEN" --header "Content-Type: application/json" --request POST --data "{\"query\": \"query {currentUser {name}}\"} +``` + +### GraphiQL + +GraphiQL (pronounced “graphical”) allows you to run queries directly against the server endpoint +with syntax highlighting and autocomplete. It also allows you to explore the schema and types. + +The examples below: + +- Can be run directly against GitLab 11.0 or later, though some of the types and fields +may not be supported in older versions. +- Will work against GitLab.com without any further setup. Make sure you are signed in and +navigate to the [GraphiQL Explorer](https://www.gitlab.com/-/graphql-explorer). + +If you want to run the queries locally, or on a self-managed instance, +you will need to either: + +- Create the `gitlab-org` group with a project called `graphql-sandbox` under it. Create +several issues within the project. +- Edit the queries to replace `gitlab-org/graphql-sandbox` with your own group and project. + +Please refer to [running GraphiQL](index.md#graphiql) for more information. + +NOTE: **Note:** +If you are running GitLab 11.0 to 12.0, enable the `graphql` +[feature flag](../features.md#set-or-create-a-feature). + +## Queries and mutations + +The GitLab GraphQL API can be used to perform: + +- Queries for data retrieval. +- [Mutations](#mutations) for creating, updating, and deleting data. + +NOTE: **Note:** +In the GitLab GraphQL API, `id` generally refers to a global ID, +which is an object identifier in the format of `gid://gitlab/Issue/123`. + +[GitLab's GraphQL Schema](reference/index.md) outlines which objects and fields are +available for clients to query and their corresponding data types. + +Example: Get only the names of all the projects the currently logged in user can access (up to a limit, more on that later) +in the group `gitlab-org`. + +```graphql +query { + group(fullPath: "gitlab-org") { + id + name + projects { + nodes { + name + } + } + } +} +``` + +Example: Get a specific project and the title of Issue #2. + +```graphql +query { + project(fullPath: "gitlab-org/graphql-sandbox") { + name + issue(iid: "2") { + title + } + } + } +``` + +### The root node + +Any field defined in [`QueryType`](https://gitlab.com/gitlab-org/gitlab/tree/master/app/graphql/types/query_type.rb) will be exposed as a root node. +When retrieving child nodes use: + +- the `edges { node { } }` syntax. +- the short form `nodes { }` syntax. + +Underneath it all is a graph we are traversing, hence the name GraphQL. + +Example: Get a project (only its name) and the titles of all its issues. + +```graphql +query { + project(fullPath: "gitlab-org/graphql-sandbox") { + name + issues { + nodes { + title + description + } + } + } +} +``` + +More on schema definitions: +[graphql-ruby docs](https://graphql-ruby.org/schema/definition) + +More about queries: +[GraphQL docs](https://graphql.org/learn/queries/) and +[graphql-ruby docs](https://graphql-ruby.org/queries/executing_queries) + +### Authorization + +Authorization uses the same engine as the Rails app. So if you've signed in to GitLab +and use GraphiQL, all queries will be performed as you, the signed in user. + +See the [authorization section](../../development/api_graphql_styleguide.html#authorization) of the StyleGuide +for implementation details. + +### Resolvers + +A resolver is how we define how the records requested by the client are retrieved, collected, +and assembled into the response. + +The [GraphQL API StyleGuide](../../development/api_graphql_styleguide.md#resolvers) has more details +about the implementation of resolvers. + +More about resolvers: +[GraphQL Docs](https://graphql.org/learn/execution/) and +[graphql-ruby docs](https://graphql-ruby.org/fields/resolvers.html) + +### Mutations + +Mutations make changes to data. We can update, delete, or create new records. Mutations +generally use InputTypes and variables, neither of which appear here. + +Mutations have: + +- Inputs. For example, arguments, such as which emoji you'd like to award, +and to which object. +- Return statements. That is, what you'd like to get back when it's successful. +- Errors. Always ask for what went wrong, just in case. + +#### Creation mutations + +Example: Let's have some tea - add a `:tea:` reaction emoji to an issue. + +```graphql +mutation { + addAwardEmoji(input: { awardableId: "gid://gitlab/Issue/27039960", + name: "tea" + }) { + awardEmoji { + name + description + unicode + emoji + unicodeVersion + user { + name + } + } + errors + } +} +``` + +Example: Add a comment to the issue (we're using the ID of the `GitLab.com` issue - but +if you're using a local instance, you'll need to get the ID of an issue you can write to). + +```graphql +mutation { + createNote(input: { noteableId: "gid://gitlab/Issue/27039960", + body: "*sips tea*" + }) { + note { + id + body + discussion { + id + } + } + errors + } +} +``` + +#### Update mutations + +When you see the result `id` of the note you created - take a note of it. Now let's edit it to sip faster! + +```graphql +mutation { + updateNote(input: { id: "gid://gitlab/Note/<note id>", + body: "*SIPS TEA*" + }) { + note { + id + body + } + errors + } +} +``` + +#### Deletion mutations + +Let's delete the comment, since our tea is all gone. + +```graphql +mutation { + destroyNote(input: { id: "gid://gitlab/Note/<note id>" }) { + note { + id + body + } + errors + } +} +``` + +You should get something like the following output: + +```json +{ + "data": { + "destroyNote": { + "errors": [], + "note": null + } + } +} +``` + +We've asked for the note details, but it doesn't exist anymore, so we get `null`. + +The [GraphQL API StyleGuide](../../development/api_graphql_styleguide.md#mutations) has more details +about implementation of mutations. + +More about mutations: +[GraphQL Docs](https://graphql.org/learn/queries/#mutations) and +[graphql-ruby docs](https://graphql-ruby.org/mutations/mutation_classes.html) + +### Introspective queries + +Clients can query the GraphQL endpoint for information about its own schema. +by making an [introspective query](https://graphql.org/learn/introspection/). + +It is through an introspection query that the [GraphiQL Query Explorer](https://gitlab.com/-/graphql-explorer) +gets all of its knowledge about our GraphQL schema to do autocompletion and provide +its interactive `Docs` tab. + +Example: Get all the type names in the schema. + +```graphql +{ + __schema { + types { + name + } + } +} +``` + +Example: Get all the fields associated with Issue. +`kind` tells us the enum value for the type, like `OBJECT`, `SCALAR` or `INTERFACE`. + +```graphql +query IssueTypes { + __type(name: "Issue") { + kind + name + fields { + name + description + type { + name + } + } + } +} +``` + +More about introspection: +[GraphQL docs](https://graphql.org/learn/introspection/) and +[graphql-ruby docs](https://graphql-ruby.org/schema/introspection.html) + +## Sorting + +Some of GitLab's GraphQL endpoints allow you to specify how you'd like a collection of +objects to be sorted. You can only sort by what the schema allows you to. + +Example: Issues can be sorted by creation date: + +```graphql +query { + project(fullPath: "gitlab-org/graphql-sandbox") { + name + issues(sort: created_asc) { + nodes { + title + createdAt + } + } + } + } +``` + +## Pagination + +Pagination is a way of only asking for a subset of the records (say, the first 10). +If we want more of them, we can make another request for the next 10 from the server +(in the form of something like "please give me the next 10 records"). + +By default, GitLab's GraphQL API will return only the first 100 records of any collection. +This can be changed by using `first` or `last` arguments. Both arguments take a value, +so `first: 10` will return the first 10 records, and `last: 10` the last 10 records. + +Example: Retrieve only the first 2 issues (slicing). The `cursor` field gives us a position from which +we can retrieve further records relative to that one. + +```graphql +query { + project(fullPath: "gitlab-org/graphql-sandbox") { + name + issues(first: 2) { + edges { + node { + title + } + } + pageInfo { + endCursor + hasNextPage + } + } + } +} +``` + +Example: Retrieve the next 3. (The cursor value +`eyJpZCI6IjI3MDM4OTMzIiwiY3JlYXRlZF9hdCI6IjIwMTktMTEtMTQgMDU6NTY6NDQgVVRDIn0` +could be different, but it's the `cursor` value returned for the second issue returned above.) + +```graphql +query { + project(fullPath: "gitlab-org/graphql-sandbox") { + name + issues(first: 3, after: "eyJpZCI6IjI3MDM4OTMzIiwiY3JlYXRlZF9hdCI6IjIwMTktMTEtMTQgMDU6NTY6NDQgVVRDIn0") { + edges { + node { + title + } + cursor + } + pageInfo { + endCursor + hasNextPage + } + } + } +} +``` + +More on pagination and cursors: +[GraphQL docs](https://graphql.org/learn/pagination/) and +[graphql-ruby docs](https://graphql-ruby.org/relay/connections.html#cursors) diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index 510b36eba8f..b5b17102836 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -4,6 +4,27 @@ > - [Always enabled](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/30444) in GitLab 12.1. +## Getting Started + +For those new to the GitLab GraphQL API, see +[Getting started with GitLab GraphQL API](getting_started.md). + +### Quick Reference + +- GitLab's GraphQL API endpoint is located at `/api/graphql`. +- Get an [introduction to GraphQL from graphql.org](https://graphql.org/). +- GitLab supports a wide range of resources, listed in the [GraphQL API Reference](reference/index.md). + +#### GraphiQL + +Explore the GraphQL API using the interactive [GraphiQL explorer](https://gitlab.com/-/graphql-explorer), +or on your self-managed GitLab instance on +`https://<your-gitlab-site.com>/-/graphql-explorer`. + +See the [GitLab GraphQL overview](getting_started.md#graphiql) for more information about the GraphiQL Explorer. + +## What is GraphQL? + [GraphQL](https://graphql.org/) is a query language for APIs that allows clients to request exactly the data they need, making it possible to get all required data in a limited number of requests. @@ -33,11 +54,16 @@ possible. ## Available queries -A first iteration of a GraphQL API includes the following queries +The GraphQL API includes the following queries at the root level: -1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID. +1. `project` : Project information, with many of its associations such as issues and merge requests also available. 1. `group` : Basic group information and epics **(ULTIMATE)** are currently supported. 1. `namespace` : Within a namespace it is also possible to fetch `projects`. +1. `currentUser`: Information about the currently logged in user. +1. `metaData`: Metadata about GitLab and the GraphQL API. + +Root-level queries are defined in +[`app/graphql/types/query_type.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/types/query_type.rb). ### Multiplex queries @@ -58,10 +84,5 @@ Machine-readable versions are also available: - [JSON format](reference/gitlab_schema.json) - [IDL format](reference/gitlab_schema.graphql) -## GraphiQL - -The API can be explored by using the GraphiQL IDE, it is available on your -instance on `gitlab.example.com/-/graphql-explorer`. - [ce-19008]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/19008 [features-api]: ../features.md diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 42446fb6ce1..d8902be92eb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2519,6 +2519,51 @@ type IssuePermissions { } """ +Autogenerated input type of IssueSetDueDate +""" +input IssueSetDueDateInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The desired due date for the issue + """ + dueDate: Time! + + """ + The iid of the issue to mutate + """ + iid: String! + + """ + The project the issue to mutate is in + """ + projectPath: ID! +} + +""" +Autogenerated return type of IssueSetDueDate +""" +type IssueSetDueDatePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Reasons why the mutation failed. + """ + errors: [String!]! + + """ + The issue after mutation + """ + issue: Issue +} + +""" Values for sorting issues """ enum IssueSort { @@ -3511,6 +3556,7 @@ type Mutation { destroyNote(input: DestroyNoteInput!): DestroyNotePayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload + issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 07b91c1af1a..fba3dcca14d 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -14113,6 +14113,33 @@ "deprecationReason": null }, { + "name": "issueSetDueDate", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "IssueSetDueDateInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "IssueSetDueDatePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "mergeRequestSetAssignees", "description": null, "args": [ @@ -14960,6 +14987,136 @@ }, { "kind": "OBJECT", + "name": "IssueSetDueDatePayload", + "description": "Autogenerated return type of IssueSetDueDate", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issue", + "description": "The issue after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "IssueSetDueDateInput", + "description": "Autogenerated input type of IssueSetDueDate", + "fields": null, + "inputFields": [ + { + "name": "projectPath", + "description": "The project the issue to mutate is in", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "iid", + "description": "The iid of the issue to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "dueDate", + "description": "The desired due date for the issue", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "MergeRequestSetLabelsPayload", "description": "Autogenerated return type of MergeRequestSetLabels", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 18d49c9a7e1..4b71c9a8eaf 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -375,6 +375,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource | | `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource | +### IssueSetDueDatePayload + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `errors` | String! => Array | Reasons why the mutation failed. | +| `issue` | Issue | The issue after mutation | + ### Label | Name | Type | Description | diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md index afefc3925cd..70179ecde29 100644 --- a/doc/api/group_badges.md +++ b/doc/api/group_badges.md @@ -26,9 +26,10 @@ GET /groups/:id/badges | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `name` | string | no | Name of the badges to return (case-sensitive). | ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/badges +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/badges?name=Coverage ``` Example response: @@ -36,21 +37,14 @@ Example response: ```json [ { + "name": "Coverage", "id": 1, "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", "rendered_image_url": "https://shields.io/my/badge", "kind": "group" - }, - { - "id": 2, - "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", - "image_url": "https://shields.io/my/badge", - "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master", - "rendered_image_url": "https://shields.io/my/badge", - "kind": "group" - }, + } ] ``` diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 90a4f8d6e26..97dc316cc96 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -18,7 +18,7 @@ GET /projects/:id/pipelines | `yaml_errors`| boolean | no | Returns pipelines with invalid configurations | | `name`| string | no | The name of the user who triggered pipelines | | `username`| string | no | The username of the user who triggered pipelines | -| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, or `user_id` (default: `id`) | +| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, `updated_at` or `user_id` (default: `id`) | | `sort` | string | no | Sort pipelines in `asc` or `desc` order (default: `desc`) | ``` diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md index 527db478a50..0cf22808036 100644 --- a/doc/api/project_badges.md +++ b/doc/api/project_badges.md @@ -23,6 +23,7 @@ GET /projects/:id/badges | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `name` | string | no | Name of the badges to return (case-sensitive). | ```bash curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/badges @@ -33,6 +34,7 @@ Example response: ```json [ { + "name": "Coverage", "id": 1, "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", @@ -41,6 +43,7 @@ Example response: "kind": "project" }, { + "name": "Pipeline", "id": 2, "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}", "image_url": "https://shields.io/my/badge", diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 73e976a6145..ce3d6247d86 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -135,6 +135,8 @@ The following job parameters can be defined inside a `default:` block: - [`before_script`](#before_script-and-after_script) - [`after_script`](#before_script-and-after_script) - [`cache`](#cache) +- [`retry`](#retry) +- [`timeout`](#timeout) - [`interruptible`](#interruptible) In the following example, the `ruby:2.5` image is set as the default for all @@ -182,6 +184,17 @@ that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters: `:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``. +If any of the script commands return an exit code different from zero, the job +will fail and further commands will not be executed. This behavior can be avoided by +storing the exit code in a variable: + +```yaml +job: + script: + - false && true; exit_code=$? + - if [ $exit_code -ne 0 ]; then echo "Previous command failed"; fi; +``` + #### YAML anchors for `script` > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/23005) in GitLab 12.5. diff --git a/doc/development/build_test_package.md b/doc/development/build_test_package.md index f58ac79b6f4..d478d6e1653 100644 --- a/doc/development/build_test_package.md +++ b/doc/development/build_test_package.md @@ -9,16 +9,16 @@ that will create: - A deb package for Ubuntu 16.04, available as a build artifact, and - A docker image, which is pushed to [Omnibus GitLab's container registry](https://gitlab.com/gitlab-org/omnibus-gitlab/container_registry) - (images titled `gitlab-foss` and `gitlab-ee` respectively and image tag is the + (images titled `gitlab-ce` and `gitlab-ee` respectively and image tag is the commit which triggered the pipeline). When you push a commit to either the GitLab CE or GitLab EE project, the pipeline for that commit will have a `build-package` manual action you can trigger. -![Manual actions](img/trigger_ss1.png) +![Manual actions](img/build_package_v12_6.png) -![Build package manual action](img/trigger_ss2.png) +![Build package manual action](img/trigger_build_package_v12_6.png) ## Specifying versions of components diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 77c57bb332d..326ac7b3a37 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -181,6 +181,10 @@ vulnerabilities must be either empty or containing: Maintainers should **never** dismiss vulnerabilities to "empty" the list, without duly verifying them. +Note that certain Merge Requests may target a stable branch. These are rare +events. These types of Merge Requests cannot be merged by the Maintainer. +Instead these should be sent to the [Release Manager](https://about.gitlab.com/community/release-managers/). + ## Best practices ### Everyone diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index e6d666473c3..d4f64b25bfa 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -1055,7 +1055,7 @@ Sometimes features are shipped with feature flags, either: - On by default, but providing the option to turn the feature off. - Off by default, but providing the option to turn the feature on. -When documenting feature flags for a feature, it's important that users know: +When documenting feature flags for a feature, include: - Why a feature flag is necessary. Some of the reasons are [outlined in the handbook](https://about.gitlab.com/handbook/product/#alpha-beta-ga). @@ -1080,6 +1080,9 @@ Feature.disable(:feature_flag) ``` ```` +For guidance on developing with feature flags, see +[Feature flags in development of GitLab](../feature_flags/index.md). + ## API Here is a list of must-have items. Use them in the exact order that appears diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index bca24e6ee0b..e13a7d1e478 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -284,7 +284,7 @@ need to test the rendered output. [Vue][vue-test] guide's to unit test show us e One should apply to be a Vue.js expert by opening an MR when the Merge Request's they create and review show: -- Deep understanding of Vue and Vuex reactivy +- Deep understanding of Vue and Vuex reactivity - Vue and Vuex code are structured according to both official and our guidelines - Full understanding of testing a Vue and Vuex application - Vuex code follows the [documented pattern](vuex.md#actions-pattern-request-and-receive-namespaces) diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index f1374b9e280..f5915f2c0a8 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -10,3 +10,6 @@ Before using feature flags for GitLab's development, read through the following: - [Process for using features flags](process.md). - [Developing with feature flags](development.md). - [Controlling feature flags](controls.md). + +When documenting feature flags, see [Feature flags](../documentation/styleguide.md#feature-flags) +in the Documentation Style Guide. diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index c8960ac0f61..903ca6ada4a 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -311,6 +311,45 @@ Developer documentation][mdn]. [mdn]: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_content_best_practices#Splitting +##### Vue components interpolation + +When translating UI text in Vue components, you might want to include child components inside +the translation string. +You could not use a JavaScript-only solution to render the translation, +because Vue would not be aware of the child components and would render them as plain text. + +For this use case, you should use the `gl-sprintf` component which is maintained +in **GitLab UI**. + +The `gl-sprintf` component accepts a `message` property, which is the translatable string, +and it exposes a named slot for every placeholder in the string, which lets you include Vue +components easily. + +Assume you want to print the translatable string +`Pipeline %{pipelineId} triggered %{timeago} by %{author}`. To replace the `%{timeago}` and +`%{author}` placeholders with Vue components, here's how you would do that with `gl-sprintf`: + +```html +<template> + <div> + <gl-sprintf :message="__('Pipeline %{pipelineId} triggered %{timeago} by %{author}')"> + <template #pipelineId>{{ pipeline.id }}</template> + <template #timeago> + <timeago :time="pipeline.triggerTime" /> + </template> + <template #author> + <gl-avatar-labeled + :src="pipeline.triggeredBy.avatarPath" + :label="pipeline.triggeredBy.name" + /> + </template> + </gl-sprintf> + </div> +</template> +``` + +For more information, see the [`gl-sprintf`](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-sprintf--default) documentation. + ## Updating the PO files with the new content Now that the new content is marked for translation, we need to update diff --git a/doc/development/img/build_package_v12_6.png b/doc/development/img/build_package_v12_6.png Binary files differnew file mode 100644 index 00000000000..c3d99e6c6ce --- /dev/null +++ b/doc/development/img/build_package_v12_6.png diff --git a/doc/development/img/trigger_build_package_v12_6.png b/doc/development/img/trigger_build_package_v12_6.png Binary files differnew file mode 100644 index 00000000000..6f5879bd8c4 --- /dev/null +++ b/doc/development/img/trigger_build_package_v12_6.png diff --git a/doc/development/img/trigger_ss1.png b/doc/development/img/trigger_ss1.png Binary files differdeleted file mode 100644 index addbc551f73..00000000000 --- a/doc/development/img/trigger_ss1.png +++ /dev/null diff --git a/doc/development/img/trigger_ss2.png b/doc/development/img/trigger_ss2.png Binary files differdeleted file mode 100644 index 02ef3810a59..00000000000 --- a/doc/development/img/trigger_ss2.png +++ /dev/null diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 764bd68000d..39384f9bcbe 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -29,6 +29,7 @@ The current stages are: - `review`: This stage includes jobs that deploy the GitLab and Docs Review Apps. - `qa`: This stage includes jobs that perform QA tasks against the Review App that is deployed in the previous stage. +- `notification`: This stage includes jobs that sends notifications about pipeline status. - `post-test`: This stage includes jobs that build reports or gather data from the previous stages' jobs (e.g. coverage, Knapsack metadata etc.). - `pages`: This stage includes a job that deploys the various reports as @@ -191,8 +192,8 @@ subgraph "`review-prepare` stage" end subgraph "`review` stage" - G --> |needs| E; - G2 --> |needs| E; + G + G2 end subgraph "`qa` stage" @@ -209,6 +210,11 @@ subgraph "`qa` stage" dast -.-> |needs and depends on| G; end +subgraph "`notification` stage" + NOTIFICATION1["schedule:package-and-qa:notify-success<br>(on_success)"] -.-> |needs| P; + NOTIFICATION2["schedule:package-and-qa:notify-failure<br>(on_failure)"] -.-> |needs| P; + end + subgraph "`post-test` stage" M end diff --git a/doc/development/shell_scripting_guide/index.md b/doc/development/shell_scripting_guide/index.md index 60678497bb2..e0895a088ab 100644 --- a/doc/development/shell_scripting_guide/index.md +++ b/doc/development/shell_scripting_guide/index.md @@ -60,7 +60,7 @@ All projects with shell scripts should use this GitLab CI/CD job: ```yaml shell check: - image: koalaman/shellcheck-alpine + image: koalaman/shellcheck-alpine:stable stage: test before_script: - shellcheck --version diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index ecfcbc731e1..eddfb561748 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -129,6 +129,10 @@ two node pools: ### Helm/Tiller +The Helm/Tiller version used is defined in the +[`registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base` image](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/Dockerfile.gitlab-charts-build-base#L4) +used by the `review-deploy` and `review-stop` jobs. + The `tiller` deployment (the Helm server) is deployed to a dedicated node pool that has the `app=helm` label and a specific [taint](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 01d86331a0a..80b8db84bed 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -273,6 +273,65 @@ git config core.sshCommand "ssh -o IdentitiesOnly=yes -i ~/.ssh/private-key-file This will not use the SSH Agent and requires at least Git 2.10. +## Multiple accounts on a single GitLab instance + +The [per-repository](#per-repository-ssh-keys) method also works for using +multiple accounts within a single GitLab instance. + +Alternatively, it is possible to directly assign aliases to hosts in +`~.ssh/config`. SSH and, by extension, Git will fail to log in if there is +an `IdentityFile` set outside of a `Host` block in `.ssh/config`. This is +due to how SSH assembles `IdentityFile` entries and is not changed by +setting `IdentitiesOnly` to `yes`. `IdentityFile` entries should point to +the private key of an SSH key pair. + +NOTE: **Note:** +Private and public keys should be readable by the user only. Accomplish this +on Linux and macOS by running: `chmod 0400 ~/.ssh/<example_ssh_key>` and +`chmod 0400 ~/.ssh/<example_sh_key.pub>`. + +```conf +# User1 Account Identity +Host <user_1.gitlab.com> + Hostname gitlab.com + PreferredAuthentications publickey + IdentityFile ~/.ssh/<example_ssh_key1> + +# User2 Account Identity +Host <user_2.gitlab.com> + Hostname gitlab.com + PreferredAuthentications publickey + IdentityFile ~/.ssh/<example_ssh_key2> +``` + +NOTE: **Note:** +The example `Host` aliases are defined as `user_1.gitlab.com` and +`user_2.gitlab.com` for efficiency and transparency. Advanced configurations +are more difficult to maintain; using this type of alias makes it easier to +understand when using other tools such as `git remote` subcommands. SSH +would understand any string as a `Host` alias thus `Tanuki1` and `Tanuki2`, +despite giving very little context as to where they point, would also work. + +Cloning the `gitlab` repository normally looks like this: + +```sh +git clone git@gitlab.com:gitlab-org/gitlab.git +``` + +To clone it for `user_1`, replace `gitlab.com` with the SSH alias `user_1.gitlab.com`: + +```sh +git clone git@<user_1.gitlab.com>:gitlab-org/gitlab.git +``` + +Fix a previously cloned repository using the `git remote` command. + +The example below assumes the remote repository is aliased as `origin`. + +```sh +git remote set-url origin git@<user_1.gitlab.com>:gitlab-org/gitlab.git +``` + ## Deploy keys ### Per-repository deploy keys diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index f775dd8bbb4..79deda73d34 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -161,6 +161,11 @@ but commented out to help encourage others to add to it in the future. --> ## Required pipeline configuration **(PREMIUM ONLY)** +CAUTION: **Caution:** +The Required Pipeline Configuration feature is deprecated and will be removed when an +[improved compliance solution](https://gitlab.com/gitlab-org/gitlab/issues/34830) +is added to GitLab. It is recommended to avoid using this feature. + GitLab administrators can force a pipeline configuration to run on every pipeline. diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md index c766ca4cffc..a42cf7f09ff 100644 --- a/doc/user/application_security/sast/analyzers.md +++ b/doc/user/application_security/sast/analyzers.md @@ -18,6 +18,7 @@ SAST supports the following official analyzers: - [`eslint`](https://gitlab.com/gitlab-org/security-products/analyzers/eslint) (ESLint (JavaScript and React)) - [`flawfinder`](https://gitlab.com/gitlab-org/security-products/analyzers/flawfinder) (Flawfinder) - [`gosec`](https://gitlab.com/gitlab-org/security-products/analyzers/gosec) (Gosec) +- [`kubesec`](https://gitlab.com/gitlab-org/security-products/analyzers/kubesec) (Kubesec) - [`nodejs-scan`](https://gitlab.com/gitlab-org/security-products/analyzers/nodejs-scan) (NodeJsScan) - [`phpcs-security-audit`](https://gitlab.com/gitlab-org/security-products/analyzers/phpcs-security-audit) (PHP CS security-audit) - [`pmd-apex`](https://gitlab.com/gitlab-org/security-products/analyzers/pmd-apex) (PMD (Apex only)) @@ -116,24 +117,24 @@ Custom analyzers are not spawned automatically when [Docker In Docker](index.md# ## Analyzers Data -| Property \ Tool | Apex | Bandit | Brakeman | ESLint security | Find Sec Bugs | Flawfinder | Go AST Scanner | NodeJsScan | Php CS Security Audit | Security code Scan (.NET) | TSLint Security | Sobelow | -| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :-------------: | :----------------: | -| Severity | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | 𐄂 | -| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Description | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | -| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| End line | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | -| Start column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | -| End column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | -| External id (e.g. CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| URLs | ✓ | 𐄂 | ✓ | 𐄂 | ⚠ | 𐄂 | ⚠ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Internal doc/explanation | ✓ | ⚠ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | -| Solution | ✓ | 𐄂 | 𐄂 | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Confidence | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | -| Affected item (e.g. class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Source code extract | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | -| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | +| Property \ Tool | Apex | Bandit | Brakeman | ESLint security | Find Sec Bugs | Flawfinder | Go AST Scanner | Kubesec Scanner | NodeJsScan | Php CS Security Audit | Security code Scan (.NET) | Sobelow | TSLint Security | +| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :----------------: | :-------------: | +| Severity | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | ✓ | +| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Description | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | +| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | ✓ | +| End line | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | +| Start column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | +| End column | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | +| External id (e.g. CVE) | 𐄂 | 𐄂 | ⚠ | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| URLs | ✓ | 𐄂 | ✓ | 𐄂 | ⚠ | 𐄂 | ⚠ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Internal doc/explanation | ✓ | ⚠ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | +| Solution | ✓ | 𐄂 | 𐄂 | 𐄂 | ⚠ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Affected item (e.g. class or package) | ✓ | 𐄂 | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Confidence | 𐄂 | ✓ | ✓ | 𐄂 | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | ✓ | 𐄂 | +| Source code extract | 𐄂 | ✓ | ✓ | ✓ | 𐄂 | ✓ | ✓ | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | 𐄂 | +| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 𐄂 | 𐄂 | ✓ | ✓ | ✓ | ✓ | - ✓ => we have that data - ⚠ => we have that data but it's partially reliable, or we need to extract it from unstructured content diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md index 8b8d578a78e..c0885e27da6 100644 --- a/doc/user/application_security/sast/index.md +++ b/doc/user/application_security/sast/index.md @@ -73,6 +73,7 @@ The following table shows which languages, package managers and frameworks are s | Groovy ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.3 (Gradle) & 11.9 (Ant, Maven, SBT) | | Java ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) | | JavaScript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 | +| Kubernetes manifests | [Kubesec](https://github.com/controlplaneio/kubesec) | 12.6 | | Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 | | PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | 10.8 | | Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 | @@ -185,6 +186,22 @@ variables: This will create individual `<analyzer-name>-sast` jobs for each analyzer that runs in your CI/CD pipeline. +#### Enabling kubesec analyzer + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12752) in GitLab Ultimate 12.6. + +When [Docker in Docker is disabled](#disabling-docker-in-docker-for-sast), +you will need to set `SCAN_KUBERNETES_MANIFESTS` to `"true"` to enable the +kubesec analyzer. In `.gitlab-ci.yml`, define: + +```yaml +include: + template: SAST.gitlab-ci.yml + +variables: + SCAN_KUBERNETES_MANIFESTS: "true" +``` + ### Available variables SAST can be [configured](#customizing-the-sast-settings) using environment variables. @@ -232,19 +249,20 @@ Timeout variables are not applicable for setups with [disabled Docker In Docker] Some analyzers can be customized with environment variables. -| Environment variable | Analyzer | Description | -|-------------------------|----------|----------| -| `ANT_HOME` | spotbugs | The `ANT_HOME` environment variable. | -| `ANT_PATH` | spotbugs | Path to the `ant` executable. | -| `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. | -| `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. | -| `JAVA_PATH` | spotbugs | Path to the `java` executable. | -| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. | -| `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. | -| `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. | -| `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). | -| `SBT_PATH` | spotbugs | Path to the `sbt` executable. | -| `FAIL_NEVER` | spotbugs | Set to `1` to ignore compilation failure. | +| Environment variable | Analyzer | Description | +|-----------------------------|----------|-------------| +| `SCAN_KUBERNETES_MANIFESTS` | kubesec | Set to `"true"` to scan Kubernetes manifests when [Docker in Docker](#disabling-docker-in-docker-for-sast) is disabled. | +| `ANT_HOME` | spotbugs | The `ANT_HOME` environment variable. | +| `ANT_PATH` | spotbugs | Path to the `ant` executable. | +| `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. | +| `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. | +| `JAVA_PATH` | spotbugs | Path to the `java` executable. | +| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. | +| `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. | +| `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. | +| `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). | +| `SBT_PATH` | spotbugs | Path to the `sbt` executable. | +| `FAIL_NEVER` | spotbugs | Set to `1` to ignore compilation failure. | #### Custom environment variables diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index c3e2e6bca5b..7ee1650f698 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -417,6 +417,18 @@ install Crossplane using the [`values.yaml`](https://github.com/crossplaneio/crossplane/blob/master/cluster/charts/crossplane/values.yaml.tmpl) file. +#### Enabling installation + +This is a preliminary release of Crossplane as a GitLab-managed application. By default, +the ability to install it is disabled. + +To allow installation of Crossplane as a GitLab-managed application, ask a GitLab +administrator to run following command within a Rails console: + +```ruby +Feature.enable(:enable_cluster_application_crossplane) +``` + ## Upgrading applications > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24789) in GitLab 11.8. diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 1fe456902a2..2b36c3bdf5b 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -75,6 +75,21 @@ NOTE: **Note:** If you [install applications](#installing-applications) on your cluster, GitLab will create the resources required to run these even if you have chosen to manage your own cluster. +### Clearing the cluster cache + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31759) in GitLab 12.6. + +If you choose to allow GitLab to manage your cluster for you, GitLab stores a cached +version of the namespaces and service accounts it creates for your projects. If you +modify these resources in your cluster manually, this cache can fall out of sync with +your cluster, which can cause deployment jobs to fail. + +To clear the cache: + +1. Navigate to your group’s **Kubernetes** page, and select your cluster. +1. Expand the **Advanced settings** section. +1. Click **Clear cluster cache**. + ## Base domain > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24580) in GitLab 11.8. diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md index 07ef8bca972..c73368fbbd2 100644 --- a/doc/user/project/clusters/add_remove_clusters.md +++ b/doc/user/project/clusters/add_remove_clusters.md @@ -206,9 +206,46 @@ GitLab supports: Before creating your first cluster on Amazon EKS with GitLab's integration, make sure the following requirements are met: +- Enable the `create_eks_clusters` feature flag for your GitLab instance. - An [Amazon Web Services](https://aws.amazon.com/) account is set up and you are able to log in. - You have permissions to manage IAM resources. +#### Enable the `create_eks_clusters` feature flag **(CORE ONLY)** + +NOTE: **Note:** +If you are running a self-managed instance, EKS cluster creation will not be available +unless the feature flag `create_eks_clusters` is enabled. This can be done from the Rails console +by instance administrators. + +Use these commands to start the Rails console: + +```sh +# Omnibus GitLab +gitlab-rails console + +# Installation from source +cd /home/git/gitlab +sudo -u git -H bin/rails console RAILS_ENV=production +``` + +Then run the following command to enable the feature flag: + +``` +Feature.enable(:create_eks_clusters) +``` + +You can also enable the feature flag only for specific projects with: + +``` +Feature.enable(:create_eks_clusters, Project.find_by_full_path('my_group/my_project')) +``` + +Run the following command to disable the feature flag: + +``` +Feature.disable(:create_eks_clusters) +``` + ##### Additional requirements for self-managed instances If you are using a self-managed GitLab instance, GitLab must first diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index c5c2c2c07e7..2aa746fc596 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -132,6 +132,21 @@ NOTE: **Note:** If you [install applications](#installing-applications) on your cluster, GitLab will create the resources required to run these even if you have chosen to manage your own cluster. +#### Clearing the cluster cache + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31759) in GitLab 12.6. + +If you choose to allow GitLab to manage your cluster for you, GitLab stores a cached +version of the namespaces and service accounts it creates for your projects. If you +modify these resources in your cluster manually, this cache can fall out of sync with +your cluster, which can cause deployment jobs to fail. + +To clear the cache: + +1. Navigate to your project’s **Operations > Kubernetes** page, and select your cluster. +1. Expand the **Advanced settings** section. +1. Click **Clear cluster cache**. + ### Base domain > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24580) in GitLab 11.8. diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md index f1a9f156d7f..a195360aa12 100644 --- a/doc/user/project/clusters/serverless/aws.md +++ b/doc/user/project/clusters/serverless/aws.md @@ -4,9 +4,20 @@ GitLab allows users to easily deploy AWS Lambda functions and create rich server GitLab supports deployment of functions to AWS Lambda using a combination of: -- [Serverless Framework](https://serverless.com) +- [Serverless Framework with AWS](https://serverless.com/framework/docs/providers/aws/) - GitLab CI/CD +We have prepared an example with a step-by-step guide to create a simple function and deploy it on AWS. + +Additionally, in the [How To section](#how-to), you can read about different use cases, +like: + +- Running a function locally. +- Working with secrets. +- Setting up CORS. + +Alternatively, you can quickly [create a new project with a template](https://docs.gitlab.com/ee/gitlab-basics/create-project.html#project-templates). The [`Serverless Framework/JS` template](https://gitlab.com/gitlab-org/project-templates/serverless-framework/) already includes all parts described below. + ## Example In the following example, you will: @@ -23,13 +34,13 @@ The example consists of the following steps: 1. Crafting the `.gitlab-ci.yml` file 1. Setting up your AWS credentials with your GitLab account 1. Deploying your function -1. Testing your function +1. Testing the deployed function Lets take it step by step. ### Creating a Lambda handler function -Your Lambda function will be the primary handler of requests. In this case we will create a very simple Node.js "Hello" function: +Your Lambda function will be the primary handler of requests. In this case we will create a very simple Node.js `hello` function: ```javascript 'use strict'; @@ -46,8 +57,6 @@ module.exports.hello = async event => { ), }; }; - - ``` Place this code in the file `src/handler.js`. @@ -58,7 +67,7 @@ In our case, `module.exports.hello` defines the `hello` handler that will be ref You can learn more about the AWS Lambda Node.js function handler and all its various options here: <https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html> -### Creating a serverless.yml file +### Creating a `serverless.yml` file In the root of your project, create a `serverless.yml` file that will contain configuration specifics for the Serverless Framework. @@ -69,7 +78,7 @@ service: gitlab-example provider: name: aws runtime: nodejs10.x - + functions: hello: handler: src/handler.hello @@ -87,7 +96,7 @@ You can read more about the available properties and additional configuration po ### Crafting the .gitlab-ci.yml file -In a `.gitlab-ci.yml` file, place the following code: +In a `.gitlab-ci.yml` file in the root of your project, place the following code: ```yaml image: node:latest @@ -109,13 +118,14 @@ This example code does the following: 1. Uses the `node:latest` image for all GitLab CI builds 1. The `deploy` stage: - -- Installs the `serverless framework`. -- Deploys the serverless function to your AWS account using the AWS credentials defined above. + - Installs the Serverless Framework. + - Deploys the serverless function to your AWS account using the AWS credentials + defined above. + - Deploys the serverless function to your AWS account using the AWS credentials defined above ### Setting up your AWS credentials with your GitLab account -In order to interact with your AWS account, the .gitlab-ci.yml requires both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` be defined in your GitLab settings under **Settings > CI/CD > Variables**. +In order to interact with your AWS account, the GitLab CI/CD pipelines require both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` to be defined in your GitLab settings under **Settings > CI/CD > Variables**. For more information please see: <https://docs.gitlab.com/ee/ci/variables/README.html#via-the-ui> NOTE: **Note:** @@ -123,7 +133,7 @@ NOTE: **Note:** ### Deploying your function -Deploying your function is very simple, just `git push` to your GitLab repository and the GitLab build pipeline will automatically deploy your function. +`git push` the changes to your GitLab repository and the GitLab build pipeline will automatically deploy your function. In your GitLab deploy stage log, there will be output containing your AWS Lambda endpoint URL. The log line will look similar to this: @@ -133,7 +143,7 @@ endpoints: GET - https://u768nzby1j.execute-api.us-east-1.amazonaws.com/production/hello ``` -### Testing your function +### Manually testing your function Running the following `curl` command should trigger your function. @@ -144,7 +154,7 @@ NOTE: **Note:** curl https://u768nzby1j.execute-api.us-east-1.amazonaws.com/production/hello ``` -Should output: +That should output: ```json { @@ -156,8 +166,123 @@ Hooray! You now have a AWS Lambda function deployed via GitLab CI. Nice work! -## Example code +## How To + +In this section, we show you how to build on the basic example to: + +- Run the function locally. +- Set up secret variables. +- Set up CORS. + +### Running function locally + +The `serverless-offline` plugin allows to run your code locally. To run your code locally: + +1. Add the following to your `serverless.yml`: + + ```yaml + plugins: + - serverless-offline + ``` + +1. Start the service by running the following command: + + ```shell + serverless offline + ``` + +Running the following `curl` command should trigger your function. + +```sh +curl http://localhost:3000/hello +``` + +It should output: + +```json +{ + "message": "Your function executed successfully!" +} +``` + +### Secret variables + +Secrets are injected into your functions using environment variables. + +By defining variables in the provider section of the `serverless.yml`, you add them to +the environment of the deployed function: + +```yaml +provider: + ... + environment: + A_VARIABLE: ${env:A_VARIABLE} +``` + +From there, you can reference them in your functions as well. +Remember to add `A_VARIABLE` to your GitLab CI variables under **Settings > CI/CD > Variables**, and it will get picked up and deployed with your function. + +NOTE: **Note:** +Anyone with access to the AWS environemnt may be able to see the values of those +variables persisted in the lambda definition. + +### Setting up CORS + +If you want to set up a web page that makes calls to your function, like we have done in the [template](https://gitlab.com/gitlab-org/project-templates/serverless-framework/), you need to deal with the Cross-Origin Resource Sharing (CORS). + +The quick way to do that is to add the `cors: true` flag to the HTTP endpoint in your `serverless.yml`: + +```yaml +functions: + hello: + handler: src/handler.hello + events: + - http: # Rewrite this part to enable CORS + path: hello + method: get + cors: true # <-- CORS here +``` + +You also need to return CORS specific headers in your function response: + +```javascript +'use strict'; + +module.exports.hello = async event => { + return { + statusCode: 200, + headers: { + // Uncomment the line below if you need access to cookies or authentication + // 'Access-Control-Allow-Credentials': true, + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify( + { + message: 'Your function executed successfully!' + }, + null, + 2 + ), + }; +}; +``` + +For more information, see the [Your CORS and API Gateway survival guide](https://serverless.com/blog/cors-api-gateway-survival-guide/) +blog post written by the Serverless Framework team. + +### Writing automated tests + +The [Serverless Framework](https://gitlab.com/gitlab-org/project-templates/serverless-framework/) +example project shows how to use Jest, Axios, and `serverless-offline` plugin to do +automated testing of both local and deployed serverless function. + +## Examples and template + +The example code is available: -To see the example code for this example please follow the link below: +- As a [cloneable repository](https://gitlab.com/gitlab-org/serverless/examples/serverless-framework-js). +- In a version with [tests and secret variables](https://gitlab.com/gitlab-org/project-templates/serverless-framework/). -- [Node.js example](https://gitlab.com/gitlab-org/serverless/examples/serverless-framework-js): Deploy a AWS Lambda Javascript function + API Gateway using Serverless Framework and GitLab CI/CD +You can also use a [template](https://docs.gitlab.com/ee/gitlab-basics/create-project.html#project-templates) +(based on the version with tests and secret variables) from within the GitLab UI (see +the `Serverless Framework/JS` template). diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 794c3030c6a..4ea46399635 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -103,8 +103,12 @@ It is possible to download the latest artifacts of a job via a well known URL so you can use it for scripting purposes. NOTE: **Note:** -The latest artifacts are considered as the artifacts created by jobs in the -latest pipeline that succeeded for the specific ref. +The latest artifacts are created by jobs in the **most recent** successful pipeline +for the specific ref. If you run two types of pipelines for the same ref, the latest +artifact will be determined by timing. For example, if a branch pipeline created +by merging a merge request runs at the same time as a scheduled pipeline, the +latest artifact will be from the pipeline that completed most recently. + Artifacts for other pipelines can be accessed with direct access to them. The structure of the URL to download the whole artifacts archive is the following: diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md index 8952f845b96..11789f7d497 100644 --- a/doc/user/project/push_options.md +++ b/doc/user/project/push_options.md @@ -31,19 +31,25 @@ git push -o <push_option> ## Push options for GitLab CI/CD -If the `ci.skip` push option is used, the commit will be pushed, but no [CI pipeline](../../ci/pipelines.md) -will be created. +You can use push options to skip a CI/CD pipeline, or pass environment variables. -| Push option | Description | -| ----------- | ----------- | -| `ci.skip` | Do not create a CI pipeline for the latest push. | +| Push option | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------- | +| `ci.skip` | Do not create a CI pipeline for the latest push. | +| `ci.variable="<name>=<value>"` | Provide [environment variables](../../ci/variables/README.md) to be used in a CI pipeline, if one is created due to the push. | -For example: +An example of using `ci.skip`: ```shell git push -o ci.skip ``` +An example of passing some environment variables for a pipeline: + +```shell +git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600" +``` + ## Push options for merge requests You can use Git push options to perform certain actions for merge requests at the same diff --git a/lib/api/badges.rb b/lib/api/badges.rb index ba554e00a16..e987c24c707 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -33,7 +33,11 @@ module API get ":id/badges" do source = find_source(source_type, params[:id]) - present_badges(source, paginate(source.badges)) + badges = source.badges + name = params[:name] + badges = badges.with_name(name) if name + + present_badges(source, paginate(badges)) end desc "Preview a badge from a #{source_type}." do @@ -80,6 +84,7 @@ module API params do requires :link_url, type: String, desc: 'URL of the badge link' requires :image_url, type: String, desc: 'URL of the badge image' + optional :name, type: String, desc: 'Name for the badge' end post ":id/badges" do source = find_source_if_admin(source_type) @@ -100,6 +105,7 @@ module API params do optional :link_url, type: String, desc: 'URL of the badge link' optional :image_url, type: String, desc: 'URL of the badge image' + optional :name, type: String, desc: 'Name for the badge' end put ":id/badges/:badge_id" do source = find_source_if_admin(source_type) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3ff74dd9d38..cba4ec2c18f 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -283,7 +283,9 @@ module API expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id - expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? } + expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do + project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project) + end expose :import_status expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| @@ -1736,6 +1738,7 @@ module API end class BasicBadgeDetails < Grape::Entity + expose :name expose :link_url expose :image_url expose :rendered_link_url do |badge, options| diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 669def2b63c..a1fce9e8b20 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -191,6 +191,7 @@ module API optional :path, type: String, desc: 'The path of the repository' optional :default_branch, type: String, desc: 'The default branch of the project' use :optional_project_params + use :optional_create_project_params use :create_params end # rubocop: disable CodeReuse/ActiveRecord diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb index 05b16672912..5eca364a697 100644 --- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb +++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb @@ -42,3 +42,5 @@ module Gitlab end end end + +Gitlab::Analytics::CycleAnalytics::DataCollector.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::DataCollector') diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb index 667d6def414..0c75a141c3c 100644 --- a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb @@ -8,6 +8,8 @@ module Gitlab class StageEvent include Gitlab::CycleAnalytics::MetricsTables + delegate :label_based?, to: :class + def initialize(params) @params = params end @@ -35,7 +37,7 @@ module Gitlab query end - def label_based? + def self.label_based? false end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb index 34c726b2254..29a2d55df1a 100644 --- a/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb +++ b/lib/gitlab/analytics/cycle_analytics/stage_query_helpers.rb @@ -9,11 +9,11 @@ module Gitlab end def zero_interval - Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) + Arel::Nodes::NamedFunction.new('CAST', [Arel.sql("'0' AS INTERVAL")]) end def round_duration_to_seconds - Arel::Nodes::Extract.new(duration, :epoch) + Arel::Nodes::NamedFunction.new('ROUND', [Arel::Nodes::Extract.new(duration, :epoch)]) end def duration diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb index 83127bde6e4..b84ae53a514 100644 --- a/lib/gitlab/ci/config/entry/default.rb +++ b/lib/gitlab/ci/config/entry/default.rb @@ -14,7 +14,8 @@ module Gitlab include ::Gitlab::Config::Entry::Inheritable ALLOWED_KEYS = %i[before_script image services - after_script cache interruptible].freeze + after_script cache interruptible + timeout retry].freeze validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -44,7 +45,15 @@ module Gitlab description: 'Set jobs interruptible default value.', inherit: false - helpers :before_script, :image, :services, :after_script, :cache, :interruptible + entry :timeout, Entry::Timeout, + description: 'Set jobs default timeout.', + inherit: false + + entry :retry, Entry::Retry, + description: 'Set retry default value.', + inherit: false + + helpers :before_script, :image, :services, :after_script, :cache, :interruptible, :timeout, :retry private diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb index 5a13fd18504..68254552f82 100644 --- a/lib/gitlab/ci/config/entry/environment.rb +++ b/lib/gitlab/ci/config/entry/environment.rb @@ -8,9 +8,11 @@ module Gitlab # Entry that represents an environment. # class Environment < ::Gitlab::Config::Entry::Node - include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Configurable - ALLOWED_KEYS = %i[name url action on_stop].freeze + ALLOWED_KEYS = %i[name url action on_stop kubernetes].freeze + + entry :kubernetes, Entry::Kubernetes, description: 'Kubernetes deployment configuration.' validations do validate do @@ -46,6 +48,7 @@ module Gitlab allow_nil: true validates :on_stop, type: String, allow_nil: true + validates :kubernetes, type: Hash, allow_nil: true end end @@ -73,6 +76,10 @@ module Gitlab value[:on_stop] end + def kubernetes + value[:kubernetes] + end + def value case @config when String then { name: @config, action: 'start' } @@ -80,6 +87,10 @@ module Gitlab else {} end end + + def skip_config_hash_validation? + true + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index c75ae87a985..7c01e6ffbe8 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -46,8 +46,6 @@ module Gitlab message: "should be one of: #{ALLOWED_WHEN.join(', ')}" } - validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) } - validates :dependencies, array_of_strings: true validates :extends, array_of_strings_or_string: true validates :rules, array_of_hashes: true @@ -103,6 +101,14 @@ module Gitlab description: 'Set jobs interruptible value.', inherit: true + entry :timeout, Entry::Timeout, + description: 'Timeout duration of this job.', + inherit: true + + entry :retry, Entry::Retry, + description: 'Retry configuration for this job.', + inherit: true + entry :only, Entry::Policy, description: 'Refs policy this job will be executed for.', default: Entry::Policy::DEFAULT_ONLY, @@ -140,10 +146,6 @@ module Gitlab description: 'Coverage configuration for this job.', inherit: false - entry :retry, Entry::Retry, - description: 'Retry configuration for this job.', - inherit: false - helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, :artifacts, :environment, :coverage, :retry, :rules, diff --git a/lib/gitlab/ci/config/entry/kubernetes.rb b/lib/gitlab/ci/config/entry/kubernetes.rb new file mode 100644 index 00000000000..2f1595d4437 --- /dev/null +++ b/lib/gitlab/ci/config/entry/kubernetes.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + class Kubernetes < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[namespace].freeze + + attributes ALLOWED_KEYS + + validations do + validates :config, type: Hash + validates :config, allowed_keys: ALLOWED_KEYS + + validates :namespace, type: String, presence: true + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/timeout.rb b/lib/gitlab/ci/config/entry/timeout.rb new file mode 100644 index 00000000000..0bffa9340de --- /dev/null +++ b/lib/gitlab/ci/config/entry/timeout.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents the interrutible value. + # + class Timeout < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) } + end + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml index a60b00b2ee8..8f50f38bbed 100644 --- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml @@ -7,6 +7,7 @@ code_quality: variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" + CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" script: - | if ! docker info &>/dev/null; then @@ -14,11 +15,12 @@ code_quality: export DOCKER_HOST='tcp://localhost:2375' fi fi + - docker pull --quiet "$CODE_QUALITY_IMAGE" - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock - "registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" /code + "$CODE_QUALITY_IMAGE" /code artifacts: reports: codequality: gl-code-quality-report.json diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb index e96f5177195..55476cd9789 100644 --- a/lib/gitlab/danger/teammate.rb +++ b/lib/gitlab/danger/teammate.rb @@ -63,7 +63,7 @@ module Gitlab def has_capability?(project, category, kind, labels) case category when :test - area = role[/Test Automation Engineer(?:.*?, (\w+))/, 1] + area = role[/Software Engineer in Test(?:.*?, (\w+))/, 1] area && labels.any?("devops::#{area.downcase}") if kind == :reviewer when :engineering_productivity diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb index 8cd9694b741..fbf252b7ec3 100644 --- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -29,10 +29,11 @@ module Gitlab def execute! result = execute_steps - if result[:status] == :success + ::Gitlab::Tracking.event("self_monitoring", "project_created") result elsif STEPS_ALLOWED_TO_FAIL.include?(result[:last_step]) + ::Gitlab::Tracking.event("self_monitoring", "project_created") success else raise StandardError, result[:message] diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb index 23d989ff258..0218f6e6232 100644 --- a/lib/gitlab/git/tag.rb +++ b/lib/gitlab/git/tag.rb @@ -62,6 +62,10 @@ module Gitlab encode! @message end + def tagger + @raw_tag.tagger + end + private def message_from_gitaly_tag diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 1dce26efc65..829e64b11a4 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -5,7 +5,7 @@ module Gitlab extend self CleanupError = Class.new(StandardError) - BG_CLEANUP_RUNTIME_S = 2 + BG_CLEANUP_RUNTIME_S = 10 FG_CLEANUP_RUNTIME_S = 0.5 MUTEX = Mutex.new @@ -107,19 +107,18 @@ module Gitlab begin cleanup_tmp_dir(tmp_dir) rescue CleanupError => e + folder_contents = Dir.children(tmp_dir) # This means we left a GPG-agent process hanging. Logging the problem in # sentry will make this more visible. Gitlab::Sentry.track_exception(e, issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918', - extra: { tmp_dir: tmp_dir }) + extra: { tmp_dir: tmp_dir, contents: folder_contents }) end tmp_keychains_removed.increment unless File.exist?(tmp_dir) end def cleanup_tmp_dir(tmp_dir) - return FileUtils.remove_entry(tmp_dir, true) if Feature.disabled?(:gpg_cleanup_retries) - # Retry when removing the tmp directory failed, as we may run into a # race condition: # The `gpg-agent` agent process may clean up some files as well while diff --git a/lib/gitlab/graphql/connections/keyset/connection.rb b/lib/gitlab/graphql/connections/keyset/connection.rb index c75ea206edb..5de075f2f7a 100644 --- a/lib/gitlab/graphql/connections/keyset/connection.rb +++ b/lib/gitlab/graphql/connections/keyset/connection.rb @@ -32,18 +32,11 @@ module Gitlab class Connection < GraphQL::Relay::BaseConnection include Gitlab::Utils::StrongMemoize - # TODO https://gitlab.com/gitlab-org/gitlab/issues/35104 - include Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection - def cursor_from_node(node) - return legacy_cursor_from_node(node) if use_legacy_pagination? - encoded_json_from_ordering(node) end def sliced_nodes - return legacy_sliced_nodes if use_legacy_pagination? - @sliced_nodes ||= begin OrderInfo.validate_ordering(ordered_nodes, order_list) @@ -137,14 +130,7 @@ module Gitlab def ordering_from_encoded_json(cursor) JSON.parse(decode(cursor)) rescue JSON::ParserError - # for the transition period where a client might request using an - # old style cursor. Once removed, make it an error: - # raise Gitlab::Graphql::Errors::ArgumentError, "Please provide a valid cursor" - # TODO can be removed in next release - # https://gitlab.com/gitlab-org/gitlab/issues/32933 - field_name = order_list.first.attribute_name - - { field_name => decode(cursor) } + raise Gitlab::Graphql::Errors::ArgumentError, "Please provide a valid cursor" end end end diff --git a/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb b/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb deleted file mode 100644 index baf900d1048..00000000000 --- a/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104 -module Gitlab - module Graphql - module Connections - module Keyset - module LegacyKeysetConnection - def legacy_cursor_from_node(node) - encode(node[legacy_order_field].to_s) - end - - # rubocop: disable CodeReuse/ActiveRecord - def legacy_sliced_nodes - @sliced_nodes ||= - begin - sliced = nodes - - sliced = sliced.where(legacy_before_slice) if before.present? - sliced = sliced.where(legacy_after_slice) if after.present? - - sliced - end - end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def use_legacy_pagination? - strong_memoize(:feature_disabled) do - Feature.disabled?(:graphql_keyset_pagination, default_enabled: true) - end - end - - def legacy_before_slice - if legacy_sort_direction == :asc - arel_table[legacy_order_field].lt(decode(before)) - else - arel_table[legacy_order_field].gt(decode(before)) - end - end - - def legacy_after_slice - if legacy_sort_direction == :asc - arel_table[legacy_order_field].gt(decode(after)) - else - arel_table[legacy_order_field].lt(decode(after)) - end - end - - def legacy_order_info - @legacy_order_info ||= nodes.order_values.first - end - - def legacy_order_field - @legacy_order_field ||= legacy_order_info&.expr&.name || nodes.primary_key - end - - def legacy_sort_direction - @legacy_order_direction ||= legacy_order_info&.direction || :desc - end - end - end - end - end -end diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb index b2fe9592c06..50fec9f3eb9 100644 --- a/lib/gitlab/import_export/attribute_cleaner.rb +++ b/lib/gitlab/import_export/attribute_cleaner.rb @@ -4,7 +4,7 @@ module Gitlab module ImportExport class AttributeCleaner ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + %w[group_id commit_id] - PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_html\Z/).freeze + PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_ids\Z/, /_html\Z/).freeze def self.clean(*args) new(*args).clean diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index e3a434dfe35..d9300da38a5 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -19,7 +19,7 @@ module Gitlab # See https://github.com/docker/distribution/blob/master/reference/regexp.go. # def container_repository_name_regex - @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-])[a-z0-9]+)*\Z} + @container_repository_regex ||= %r{\A[a-z0-9]+((?:[._/]|__|[-]{0,10})[a-z0-9]+)*\Z} end ## diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb index bd819843bd4..7bfb0d54d80 100644 --- a/lib/gitlab/sidekiq_middleware/metrics.rb +++ b/lib/gitlab/sidekiq_middleware/metrics.rb @@ -7,14 +7,17 @@ module Gitlab # timeframes than the DEFAULT_BUCKET definition. Defined in seconds. SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze + TRUE_LABEL = "yes" + FALSE_LABEL = "no" + def initialize @metrics = init_metrics @metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i) end - def call(_worker, job, queue) - labels = create_labels(queue) + def call(worker, job, queue) + labels = create_labels(worker.class, queue) queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job) @metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration @@ -42,7 +45,7 @@ module Gitlab @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded # job_status: done, fail match the job_status attribute in structured logging - labels[:job_status] = job_succeeded ? :done : :fail + labels[:job_status] = job_succeeded ? "done" : "fail" @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime) @metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time) end @@ -62,10 +65,24 @@ module Gitlab } end - def create_labels(queue) - { - queue: queue - } + def create_labels(worker_class, queue) + labels = { queue: queue.to_s, latency_sensitive: FALSE_LABEL, external_dependencies: FALSE_LABEL, feature_category: "", boundary: "" } + return labels unless worker_class.include? WorkerAttributes + + labels[:latency_sensitive] = bool_as_label(worker_class.latency_sensitive_worker?) + labels[:external_dependencies] = bool_as_label(worker_class.worker_has_external_dependencies?) + + feature_category = worker_class.get_feature_category + labels[:feature_category] = feature_category.to_s + + resource_boundary = worker_class.get_worker_resource_boundary + labels[:boundary] = resource_boundary == :unknown ? "" : resource_boundary.to_s + + labels + end + + def bool_as_label(value) + value ? TRUE_LABEL : FALSE_LABEL end def get_thread_cputime diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 909c0bdcacd..fa8f36220f1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -374,6 +374,9 @@ msgstr[1] "" msgid "%{tabname} changed" msgstr "" +msgid "%{tag}-evidence.json" +msgstr "" + msgid "%{template_project_id} is unknown or invalid" msgstr "" @@ -1780,6 +1783,15 @@ msgstr "" msgid "Analytics" msgstr "" +msgid "Analyze a review version of your web application." +msgstr "" + +msgid "Analyze your dependencies for known vulnerabilities" +msgstr "" + +msgid "Analyze your source code for known vulnerabilities" +msgstr "" + msgid "Ancestors" msgstr "" @@ -2383,6 +2395,9 @@ msgstr "" msgid "Badges|Link" msgstr "" +msgid "Badges|Name" +msgstr "" + msgid "Badges|No badge image" msgstr "" @@ -3112,6 +3127,9 @@ msgstr "" msgid "Check your .gitlab-ci.yml" msgstr "" +msgid "Check your Docker images for known vulnerabilities" +msgstr "" + msgid "Checking %{text} availability…" msgstr "" @@ -3451,6 +3469,9 @@ msgstr "" msgid "Cluster Health" msgstr "" +msgid "Cluster cache cleared." +msgstr "" + msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgstr "" @@ -3583,6 +3604,12 @@ msgstr "" msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgstr "" +msgid "ClusterIntegration|Clear cluster cache" +msgstr "" + +msgid "ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts." +msgstr "" + msgid "ClusterIntegration|Cloud Run" msgstr "" @@ -4551,6 +4578,9 @@ msgstr "" msgid "Container Registry" msgstr "" +msgid "Container Scanning" +msgstr "" + msgid "Container registry images" msgstr "" @@ -5236,14 +5266,15 @@ msgid_plural "CycleAnalytics|%d projects selected" msgstr[0] "" msgstr[1] "" -msgid "CycleAnalytics|%{stageName}" -msgid_plural "CycleAnalytics|%d stages selected" -msgstr[0] "" -msgstr[1] "" +msgid "CycleAnalytics|%{stageCount} stages selected" +msgstr "" msgid "CycleAnalytics|All stages" msgstr "" +msgid "CycleAnalytics|No stages selected" +msgstr "" + msgid "CycleAnalytics|Stages" msgstr "" @@ -5527,6 +5558,9 @@ msgstr "" msgid "Dependency Proxy" msgstr "" +msgid "Dependency Scanning" +msgstr "" + msgid "Dependency proxy" msgstr "" @@ -6028,6 +6062,9 @@ msgstr "" msgid "Download codes" msgstr "" +msgid "Download evidence JSON" +msgstr "" + msgid "Download export" msgstr "" @@ -6067,6 +6104,9 @@ msgstr "" msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below." msgstr "" +msgid "Dynamic Application Security Testing (DAST)" +msgstr "" + msgid "Each Runner can be in one of the following states:" msgstr "" @@ -6613,6 +6653,9 @@ msgstr "" msgid "Epic" msgstr "" +msgid "Epic cannot be found." +msgstr "" + msgid "Epic events" msgstr "" @@ -6910,6 +6953,9 @@ msgstr "" msgid "Events" msgstr "" +msgid "Events: %{count}" +msgstr "" + msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again." msgstr "" @@ -6949,6 +6995,9 @@ msgstr "" msgid "Everything you need to create a GitLab Pages site using plain HTML." msgstr "" +msgid "Evidence collection" +msgstr "" + msgid "Example: @sub\\.company\\.com$" msgstr "" @@ -7566,6 +7615,9 @@ msgstr "" msgid "First seen" msgstr "" +msgid "First seen: %{first_seen}" +msgstr "" + msgid "Fixed date" msgstr "" @@ -7647,7 +7699,7 @@ msgstr "" msgid "ForkedFromProjectPath|Forked from" msgstr "" -msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)" +msgid "ForkedFromProjectPath|Forked from an inaccessible project" msgstr "" msgid "Forking in progress" @@ -9571,6 +9623,9 @@ msgstr "" msgid "Issue board focus mode" msgstr "" +msgid "Issue cannot be found." +msgstr "" + msgid "Issue events" msgstr "" @@ -10005,6 +10060,9 @@ msgstr "" msgid "Last seen" msgstr "" +msgid "Last seen: %{last_seen}" +msgstr "" + msgid "Last successful update" msgstr "" @@ -15038,6 +15096,9 @@ msgstr "" msgid "Search users or groups" msgstr "" +msgid "Search your project dependencies for their licenses and apply policies" +msgstr "" + msgid "Search your projects" msgstr "" @@ -15156,6 +15217,9 @@ msgstr "" msgid "Security & Compliance" msgstr "" +msgid "Security Configuration" +msgstr "" + msgid "Security Dashboard" msgstr "" @@ -15291,7 +15355,7 @@ msgstr "" msgid "SecurityDashboard|More information" msgstr "" -msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered" +msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}" msgstr "" msgid "SecurityDashboard|Project" @@ -15471,6 +15535,9 @@ msgstr "" msgid "Sentry event" msgstr "" +msgid "Sentry event: %{external_url}" +msgstr "" + msgid "Sep" msgstr "" @@ -16376,6 +16443,12 @@ msgstr "" msgid "Stage changes" msgstr "" +msgid "Stage data updated" +msgstr "" + +msgid "Stage removed" +msgstr "" + msgid "Staged" msgstr "" @@ -16499,6 +16572,9 @@ msgstr "" msgid "State your message to activate" msgstr "" +msgid "Static Application Security Testing (SAST)" +msgstr "" + msgid "Statistics" msgstr "" @@ -17482,6 +17558,9 @@ msgstr "" msgid "There was an error removing the e-mail." msgstr "" +msgid "There was an error removing your custom stage, please try again" +msgstr "" + msgid "There was an error resetting group pipeline minutes." msgstr "" @@ -17614,6 +17693,9 @@ msgstr "" msgid "This environment has no deployments yet." msgstr "" +msgid "This epic already has the maximum number of child epics." +msgstr "" + msgid "This epic does not exist or you don't have sufficient permission." msgstr "" @@ -17827,7 +17909,10 @@ msgstr "" msgid "This will redirect you to an external sign in page." msgstr "" -msgid "This will remove the fork relationship to source project" +msgid "This will remove the fork relationship between this project and %{fork_source}." +msgstr "" + +msgid "This will remove the fork relationship between this project and other projects in the fork network." msgstr "" msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here." @@ -19154,6 +19239,9 @@ msgstr "" msgid "Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use." msgstr "" +msgid "Users: %{user_count}" +msgstr "" + msgid "UsersSelect|%{name} + %{length} more" msgstr "" @@ -19420,12 +19508,6 @@ msgstr "" msgid "Want to see the data? Please ask an administrator for access." msgstr "" -msgid "We can't find an epic that matches what you are looking for." -msgstr "" - -msgid "We can't find an issue that matches what you are looking for." -msgstr "" - msgid "We could not determine the path to remove the epic" msgstr "" @@ -19746,7 +19828,7 @@ msgstr "" msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" -msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgid "You are going to remove the fork relationship from %{project_full_name}. Are you ABSOLUTELY sure?" msgstr "" msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?" diff --git a/package.json b/package.json index e55b525753a..e5f36099833 100644 --- a/package.json +++ b/package.json @@ -38,10 +38,10 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.6.2", "@gitlab/svgs": "^1.82.0", - "@gitlab/ui": "7.15.2", + "@gitlab/ui": "7.16.1", "@gitlab/visual-review-tools": "1.2.0", "@sentry/browser": "^5.7.1", - "@sourcegraph/code-host-integration": "^0.0.13", + "@sourcegraph/code-host-integration": "^0.0.14", "apollo-cache-inmemory": "^1.6.3", "apollo-client": "^2.6.4", "apollo-link": "^1.2.11", @@ -65,16 +65,8 @@ "cropper": "^2.3.0", "css-loader": "^1.0.0", "d3": "^4.13.0", - "d3-array": "^1.2.1", - "d3-axis": "^1.0.8", - "d3-brush": "^1.0.4", - "d3-ease": "^1.0.3", "d3-scale": "^1.0.7", "d3-selection": "^1.2.0", - "d3-shape": "^1.2.0", - "d3-time": "^1.0.8", - "d3-time-format": "^2.1.1", - "d3-transition": "^1.1.1", "dateformat": "^3.0.3", "deckar01-task_list": "^2.2.1", "diff": "^3.4.0", diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb index a75ea63eca7..88d6ef02d22 100644 --- a/qa/qa/page/dashboard/snippet/show.rb +++ b/qa/qa/page/dashboard/snippet/show.rb @@ -6,8 +6,8 @@ module QA module Snippet class Show < Page::Base view 'app/views/shared/snippets/_header.html.haml' do - element :snippet_title - element :snippet_description + element :snippet_title, required: true + element :snippet_description, required: true element :embed_type element :snippet_box end @@ -21,15 +21,11 @@ module QA end def has_snippet_title?(snippet_title) - within_element(:snippet_title) do - has_text?(snippet_title) - end + has_element? :snippet_title, text: snippet_title end def has_snippet_description?(snippet_description) - within_element(:snippet_description) do - has_text?(snippet_description) - end + has_element? :snippet_description, text: snippet_description end def has_embed_type?(embed_type) diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb index 9bb1c702576..d381c13ef5b 100644 --- a/qa/qa/page/issuable/sidebar.rb +++ b/qa/qa/page/issuable/sidebar.rb @@ -5,19 +5,20 @@ module QA module Issuable class Sidebar < Page::Base view 'app/views/shared/issuable/_sidebar.html.haml' do - element :labels_block, ".issuable-show-labels" # rubocop:disable QA/ElementWithPattern - element :milestones_block, '.block.milestone' # rubocop:disable QA/ElementWithPattern + element :labels_block + element :milestone_block + element :milestone_title end def has_label?(label) - page.within('.issuable-show-labels') do - !!find('span', text: label) + within_element(:labels_block) do + has_element?(:label, label_name: label) end end def has_milestone?(milestone) - page.within('.block.milestone') do - !!find("[href*='/milestones/']", text: milestone) + within_element(:milestone_block) do + has_element?(:milestone_title, text: milestone) end end end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 6e266e26d78..cb3421f93c2 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -42,6 +42,10 @@ module QA element :login_page, required: true end + def can_sign_in? + has_element?(:sign_in_button) + end + def sign_in_using_credentials(user: nil, skip_page_validation: false) # Don't try to log-in if we're already logged-in return if Page::Main::Menu.perform(&:signed_in?) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 14b8c420b16..54a08d911cb 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -122,9 +122,8 @@ module QA end def has_label?(label) - page.within(element_selector_css(:labels_block)) do - element = find('span', text: label) - !element.nil? + within_element(:labels_block) do + !!has_element?(:label, label_name: label) end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb index 6556c28ccab..d8233fc5586 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -14,11 +14,11 @@ module QA Support::Retrier.retry_until(sleep_interval: 0.5) do Page::Main::Menu.perform(&:sign_out) - Page::Main::Login.perform(&:has_sign_in_tab?) + Page::Main::Login.perform(&:can_sign_in?) end Page::Main::Login.perform do |form| - expect(form.sign_in_tab?).to be(true) + expect(form.can_sign_in?).to be(true) end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb index a118176eb8a..4fd80c353fb 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Manage', :orchestrated, :oauth do + # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/36305 + context 'Manage', :orchestrated, :oauth, :skip do describe 'OAuth login' do it 'User logs in to GitLab with GitHub OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 6969f123f95..6bdec232bb0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -28,7 +28,8 @@ module QA end end - it 'creates a merge request with a milestone and label' do + # Failure issue (in master): https://gitlab.com/gitlab-org/gitlab/issues/37304 + it 'creates a merge request with a milestone and label', :quarantine do gitlab_account_username = "@#{Runtime::User.username}" milestone = Resource::ProjectMilestone.fabricate_via_api! do |milestone| diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb index c69ce10f1c5..7aaa9299362 100644 --- a/rubocop/cop/graphql/authorize_types.rb +++ b/rubocop/cop/graphql/authorize_types.rb @@ -34,7 +34,10 @@ module RuboCop end def whitelisted?(class_node) - return false unless class_node&.const_name + class_const = class_node&.const_name + + return false unless class_const + return true if class_const.end_with?('Enum') WHITELISTED_TYPES.any? { |whitelisted| class_node.const_name.include?(whitelisted) } end diff --git a/scripts/notify-slack b/scripts/notify-slack new file mode 100755 index 00000000000..5907fd8b986 --- /dev/null +++ b/scripts/notify-slack @@ -0,0 +1,14 @@ +#!/bin/bash +# Sends Slack notification MSG to CI_SLACK_WEBHOOK_URL (which needs to be set). +# ICON_EMOJI needs to be set to an icon emoji name (without the `:` around it). + +CHANNEL=$1 +MSG=$2 +ICON_EMOJI=$3 + +if [ -z "$CHANNEL" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ] || [ -z "$MSG" ] || [ -z "$ICON_EMOJI" ]; then + echo "Missing argument(s) - Use: $0 channel message icon_emoji" + echo "and set CI_SLACK_WEBHOOK_URL environment variable." +else + curl -X POST --data-urlencode 'payload={"channel": "#'"$CHANNEL"'", "username": "GitLab QA Bot", "text": "'"$MSG"'", "icon_emoji": "'":$ICON_EMOJI:"'"}' "$CI_SLACK_WEBHOOK_URL" +fi diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index be8d5296104..79f44e1c5d0 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -141,6 +141,7 @@ function install_tiller() { --tiller-namespace "${namespace}" \ --wait \ --upgrade \ + --force-upgrade \ --node-selectors "app=helm" \ --replicas 3 \ --override "spec.template.spec.tolerations[0].key"="dedicated" \ @@ -214,6 +215,21 @@ function create_application_secret() { --dry-run -o json | kubectl apply -f - } +function label_application_secret() { + local namespace="${KUBE_NAMESPACE}" + local release="${CI_ENVIRONMENT_SLUG}" + + echoinfo "Labeling the ${release}-gitlab-initial-root-password and ${release}-gitlab-license secrets in the ${namespace} namespace..." true + + kubectl label secret --namespace "${namespace}" \ + "${release}-gitlab-initial-root-password" \ + release="${release}" + + kubectl label secret --namespace "${namespace}" \ + "${release}-gitlab-license" \ + release="${release}" +} + function download_chart() { echoinfo "Downloading the GitLab chart..." true @@ -254,6 +270,7 @@ function deploy() { gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${edition}" create_application_secret + label_application_secret HELM_CMD=$(cat << EOF helm upgrade \ diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index ebae931764d..326d0808092 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -448,6 +448,33 @@ describe Admin::ClustersController do end end + describe 'DELETE clear cluster cache' do + let(:cluster) { create(:cluster, :instance) } + let!(:kubernetes_namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + project: create(:project) + ) + end + + def go + delete :clear_cache, params: { id: cluster } + end + + it 'deletes the namespaces associated with the cluster' do + expect { go }.to change { Clusters::KubernetesNamespace.count } + + expect(response).to redirect_to(admin_cluster_path(cluster)) + expect(cluster.kubernetes_namespaces).to be_empty + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end + describe 'GET #cluster_status' do let(:cluster) { create(:cluster, :providing_by_gcp, :instance) } diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index d027405703b..d1669c84e3e 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -516,6 +516,42 @@ describe Groups::ClustersController do end end + describe 'DELETE clear cluster cache' do + let(:cluster) { create(:cluster, :group, groups: [group]) } + let!(:kubernetes_namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + project: create(:project) + ) + end + + def go + delete :clear_cache, + params: { + group_id: group, + id: cluster + } + end + + it 'deletes the namespaces associated with the cluster' do + expect { go }.to change { Clusters::KubernetesNamespace.count } + + expect(response).to redirect_to(group_cluster_path(group, cluster)) + expect(cluster.kubernetes_namespaces).to be_empty + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(group) } + it { expect { go }.to be_allowed_for(:maintainer).of(group) } + it { expect { go }.to be_denied_for(:developer).of(group) } + it { expect { go }.to be_denied_for(:reporter).of(group) } + it { expect { go }.to be_denied_for(:guest).of(group) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end + describe 'GET cluster_status' do let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 5a0512a042e..9c21b472c15 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -517,6 +517,38 @@ describe Projects::ClustersController do end end + describe 'DELETE clear cluster cache' do + let(:cluster) { create(:cluster, :project, projects: [project]) } + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster) } + + def go + delete :clear_cache, + params: { + namespace_id: project.namespace, + project_id: project, + id: cluster + } + end + + it 'deletes the namespaces associated with the cluster' do + expect { go }.to change { Clusters::KubernetesNamespace.count } + + expect(response).to redirect_to(project_cluster_path(project, cluster)) + expect(cluster.kubernetes_namespaces).to be_empty + end + + describe 'security' do + it { expect { go }.to be_allowed_for(:admin) } + it { expect { go }.to be_allowed_for(:owner).of(project) } + it { expect { go }.to be_allowed_for(:maintainer).of(project) } + it { expect { go }.to be_denied_for(:developer).of(project) } + it { expect { go }.to be_denied_for(:reporter).of(project) } + it { expect { go }.to be_denied_for(:guest).of(project) } + it { expect { go }.to be_denied_for(:user) } + it { expect { go }.to be_denied_for(:external) } + end + end + describe 'GET cluster_status' do let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) } diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb index f077b4c99fc..7e5237facf6 100644 --- a/spec/controllers/projects/tags_controller_spec.rb +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -13,7 +13,7 @@ describe Projects::TagsController do end it 'returns the tags for the page' do - expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) + expect(assigns(:tags).map(&:name)).to include('v1.1.0', 'v1.0.0') end it 'returns releases matching those tags' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index ff0259cd40d..d16201fff5a 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -837,8 +837,7 @@ describe ProjectsController do get :refs, params: { namespace_id: project.namespace, id: project, sort: 'updated_desc' } expect(json_response['Branches']).to include('master') - expect(json_response['Tags'].first).to eq('v1.1.0') - expect(json_response['Tags'].last).to eq('v1.0.0') + expect(json_response['Tags']).to include('v1.0.0') expect(json_response['Commits']).to be_nil end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index eb6f0f27917..cf00351b231 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,6 +6,7 @@ FactoryBot.define do label list_type { :label } max_issue_count { 0 } + max_issue_weight { 0 } sequence(:position) end diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb index ba7abd3af2c..9b040271468 100644 --- a/spec/features/merge_request/user_expands_diff_spec.rb +++ b/spec/features/merge_request/user_expands_diff_spec.rb @@ -8,6 +8,7 @@ describe 'User expands diff', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes) allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes) @@ -20,7 +21,7 @@ describe 'User expands diff', :js do it_behaves_like 'rendering a single diff version' it 'allows user to expand diff' do - page.within find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do + page.within find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9"]') do click_link 'Click to expand it.' wait_for_requests diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 7cb46d90092..9cbea8a8466 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -21,6 +21,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) end it_behaves_like 'rendering a single diff version' diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index e882b401122..70afe056c64 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -22,6 +22,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) project.add_maintainer(user) sign_in user diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb index 82dd779577c..de142344c26 100644 --- a/spec/features/merge_request/user_sees_diff_spec.rb +++ b/spec/features/merge_request/user_sees_diff_spec.rb @@ -11,6 +11,7 @@ describe 'Merge request > User sees diff', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) end it_behaves_like 'rendering a single diff version' diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index c3fce9761df..b3aef601c7b 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -17,6 +17,7 @@ describe 'Merge request > User sees versions', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) project.add_maintainer(user) sign_in(user) diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index 5a29477e597..313f438e23b 100644 --- a/spec/features/merge_request/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb @@ -10,6 +10,7 @@ describe 'User views diffs', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) visit(diffs_project_merge_request_path(project, merge_request)) wait_for_requests diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb index 891b780a100..684793ce116 100644 --- a/spec/features/projects/snippets/create_snippet_spec.rb +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -52,7 +52,7 @@ describe 'Projects > Snippets > Create Snippet', :js do expect(link).to match(%r{/#{Regexp.escape(project.full_path) }/uploads/\h{32}/banana_sample\.gif\z}) end - it 'creates a snippet when all reuiqred fields are filled in after validation failing' do + it 'creates a snippet when all required fields are filled in after validation failing' do fill_in 'project_snippet_title', with: 'My Snippet Title' click_button('Create snippet') diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb index 63f97eeb4e0..b1cb7685f63 100644 --- a/spec/features/projects/tags/user_edits_tags_spec.rb +++ b/spec/features/projects/tags/user_edits_tags_spec.rb @@ -21,23 +21,21 @@ describe 'Project > Tags', :js do context 'page with tags list' do it 'shows tag name' do - page.within first('.tags > .content-list > li') do - expect(page.find('.row-main-content')).to have_content 'v1.1.0 Version 1.1.0' - end + expect(page).to have_content 'v1.1.0 Version 1.1.0' end it 'shows tag edit button' do - page.within first('.tags > .content-list > li') do - edit_btn = page.find('.row-fixed-content.controls a.btn-edit') + page.within '.tags > .content-list' do + edit_btn = page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']") - expect(edit_btn['href']).to have_content '/tags/v1.1.0/release/edit' + expect(edit_btn['href']).to end_with("/#{project.full_path}/-/tags/v1.1.0/release/edit") end end end context 'edit tag release notes' do before do - find('.tags > .content-list > li:first-child .row-fixed-content.controls a.btn-edit').click + page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click end it 'shows tag name header' do diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index 832985f1a30..c2d4cefad12 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -10,6 +10,7 @@ describe 'View on environment', :js do before do stub_feature_flags(single_mr_diff_view: false) + stub_feature_flags(diffs_batch_load: false) project.add_maintainer(user) end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 90e48f3c230..47f32e0113c 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -202,13 +202,13 @@ describe 'Project' do expect(page).not_to have_content('Forked from') end - it 'shows the name of the deleted project when the source was deleted', :sidekiq_might_not_need_inline do + it 'does not show the name of the deleted project when the source was deleted', :sidekiq_might_not_need_inline do forked_project Projects::DestroyService.new(base_project, base_project.owner).execute visit project_path(forked_project) - expect(page).to have_content("Forked from #{base_project.full_name} (deleted)") + expect(page).to have_content('Forked from an inaccessible project') end context 'a fork of a fork' do diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb index 0fc62a578f9..50eac8ddaed 100644 --- a/spec/features/tags/developer_deletes_tag_spec.rb +++ b/spec/features/tags/developer_deletes_tag_spec.rb @@ -17,7 +17,7 @@ describe 'Developer deletes tag' do it 'deletes the tag' do expect(page).to have_content 'v1.1.0' - delete_first_tag + delete_tag 'v1.1.0' expect(page).not_to have_content 'v1.1.0' end @@ -46,15 +46,15 @@ describe 'Developer deletes tag' do end it 'shows the error message' do - delete_first_tag + delete_tag 'v1.1.0' expect(page).to have_content('Do not delete tags') end end - def delete_first_tag + def delete_tag(tag) page.within('.content') do - accept_confirm { first('.btn-remove').click } + accept_confirm { find("li > .row-fixed-content.controls a.btn-remove[href='/#{project.full_path}/-/tags/#{tag}']").click } end end end diff --git a/spec/features/tags/developer_updates_tag_spec.rb b/spec/features/tags/developer_updates_tag_spec.rb index 0cdd953b9ae..167079c3f31 100644 --- a/spec/features/tags/developer_updates_tag_spec.rb +++ b/spec/features/tags/developer_updates_tag_spec.rb @@ -15,9 +15,7 @@ describe 'Developer updates tag' do context 'from the tags list page' do it 'updates the release notes' do - page.within(first('.content-list .controls')) do - click_link 'Edit release notes' - end + find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click fill_in 'release_description', with: 'Awesome release notes' click_button 'Save changes' diff --git a/spec/finders/jobs_finder_spec.rb b/spec/finders/jobs_finder_spec.rb new file mode 100644 index 00000000000..675d170b90e --- /dev/null +++ b/spec/finders/jobs_finder_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe JobsFinder, '#execute' do + set(:user) { create(:user) } + set(:admin) { create(:user, :admin) } + set(:project) { create(:project, :private, public_builds: false) } + set(:pipeline) { create(:ci_pipeline, project: project) } + set(:job_1) { create(:ci_build) } + set(:job_2) { create(:ci_build, :running) } + set(:job_3) { create(:ci_build, :success, pipeline: pipeline) } + + let(:params) { {} } + + context 'no project' do + subject { described_class.new(current_user: admin, params: params).execute } + + it 'returns all jobs' do + expect(subject).to match_array([job_1, job_2, job_3]) + end + + context 'non admin user' do + let(:admin) { user } + + it 'returns no jobs' do + expect(subject).to be_empty + end + end + + context 'without user' do + let(:admin) { nil } + + it 'returns no jobs' do + expect(subject).to be_empty + end + end + + context 'scope is present' do + let(:jobs) { [job_1, job_2, job_3] } + + where(:scope, :index) do + [ + ['pending', 0], + ['running', 1], + ['finished', 2] + ] + end + + with_them do + let(:params) { { scope: scope } } + + it { expect(subject).to match_array([jobs[index]]) } + end + end + end + + context 'a project is present' do + subject { described_class.new(current_user: user, project: project, params: params).execute } + + context 'user has access to the project' do + before do + project.add_maintainer(user) + end + + it 'returns jobs for the specified project' do + expect(subject).to match_array([job_3]) + end + end + + context 'user has no access to project builds' do + before do + project.add_guest(user) + end + + it 'returns no jobs' do + expect(subject).to be_empty + end + end + + context 'without user' do + let(:user) { nil } + + it 'returns no jobs' do + expect(subject).to be_empty + end + end + end +end diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb index e9f29ab2441..582d82bbf79 100644 --- a/spec/finders/tags_finder_spec.rb +++ b/spec/finders/tags_finder_spec.rb @@ -95,24 +95,25 @@ describe TagsFinder do end context 'filter and sort' do - it 'filters tags by name and sorts by recently_updated' do - params = { sort: 'updated_desc', search: 'v1' } - tags_finder = described_class.new(repository, params) + let(:tags_to_compare) { %w[v1.0.0 v1.1.0] } + subject { described_class.new(repository, params).execute.select { |tag| tags_to_compare.include?(tag.name) } } - result = tags_finder.execute + context 'when sort by updated_desc' do + let(:params) { { sort: 'updated_desc', search: 'v1' } } - expect(result.first.name).to eq('v1.1.0') - expect(result.count).to eq(2) + it 'filters tags by name' do + expect(subject.first.name).to eq('v1.1.0') + expect(subject.count).to eq(2) + end end - it 'filters tags by name and sorts by last_updated' do - params = { sort: 'updated_asc', search: 'v1' } - tags_finder = described_class.new(repository, params) - - result = tags_finder.execute + context 'when sort by updated_asc' do + let(:params) { { sort: 'updated_asc', search: 'v1' } } - expect(result.first.name).to eq('v1.0.0') - expect(result.count).to eq(2) + it 'filters tags by name' do + expect(subject.first.name).to eq('v1.0.0') + expect(subject.count).to eq(2) + end end end end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 7603892e198..760dcb96252 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -36,7 +36,8 @@ }, "title": { "type": "string" }, "position": { "type": ["integer", "null"] }, - "max_issue_count": { "type": "integer" } + "max_issue_count": { "type": "integer" }, + "max_issue_weight": { "type": "integer" } }, "additionalProperties": true } diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json index 8dc3999baa2..e4933ee0b93 100644 --- a/spec/fixtures/api/schemas/public_api/v4/board.json +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -77,7 +77,8 @@ } }, "position": { "type": ["integer", "null"] }, - "max_issue_count": { "type": "integer" } + "max_issue_count": { "type": "integer" }, + "max_issue_weight": { "type": "integer" } }, "additionalProperties": false } diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js index ddc17dea291..cf91c840cc4 100644 --- a/spec/frontend/error_tracking/components/error_details_spec.js +++ b/spec/frontend/error_tracking/components/error_details_spec.js @@ -83,13 +83,34 @@ describe('ErrorDetails', () => { expect(wrapper.find(Stacktrace).exists()).toBe(false); }); - it('should allow a blank issue to be created', () => { + it('should allow an issue to be created with title and description', () => { store.state.details.loading = false; - store.state.details.error.id = 1; + store.state.details.error = { + id: 1, + title: 'Issue title', + external_url: 'http://sentry.gitlab.net/gitlab', + first_seen: '2017-05-26T13:32:48Z', + last_seen: '2018-05-26T13:32:48Z', + count: 12, + user_count: 2, + }; mountComponent(); const button = wrapper.find(GlButton); + const title = 'Issue title'; + const url = 'Sentry event: http://sentry.gitlab.net/gitlab'; + const firstSeen = 'First seen: 2017-05-26T13:32:48Z'; + const lastSeen = 'Last seen: 2018-05-26T13:32:48Z'; + const count = 'Events: 12'; + const userCount = 'Users: 2'; + + const issueDescription = `${url}${firstSeen}${lastSeen}${count}${userCount}`; + + const issueLink = `/test-project/issues/new?issue[title]=${encodeURIComponent( + title, + )}&issue[description]=${encodeURIComponent(issueDescription)}`; + expect(button.exists()).toBe(true); - expect(button.attributes().href).toBe(wrapper.props().issueProjectPath); + expect(button.attributes().href).toBe(issueLink); }); describe('Stacktrace', () => { diff --git a/spec/frontend/fixtures/test_report.rb b/spec/frontend/fixtures/test_report.rb new file mode 100644 index 00000000000..d26bba9b9d0 --- /dev/null +++ b/spec/frontend/fixtures/test_report.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Projects::PipelinesController, "(JavaScript fixtures)", type: :controller do + include JavaScriptFixturesHelpers + + let(:namespace) { create(:namespace, name: "frontend-fixtures") } + let(:project) { create(:project, :repository, namespace: namespace, path: "pipelines-project") } + let(:commit) { create(:commit, project: project) } + let(:user) { create(:user, developer_projects: [project], email: commit.author_email) } + let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project, user: user) } + + render_views + + before do + sign_in(user) + stub_feature_flags(junit_pipeline_view: true) + end + + it "pipelines/test_report.json" do + get :test_report, params: { + namespace_id: project.namespace, + project_id: project, + id: pipeline.id + }, format: :json + + expect(response).to be_successful + end +end diff --git a/spec/frontend/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js index 8e0433d3fb7..5e6570f72e0 100644 --- a/spec/frontend/jobs/components/erased_block_spec.js +++ b/spec/frontend/jobs/components/erased_block_spec.js @@ -1,23 +1,30 @@ -import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; import { getTimeago } from '~/lib/utils/datetime_utility'; -import component from '~/jobs/components/erased_block.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import ErasedBlock from '~/jobs/components/erased_block.vue'; describe('Erased block', () => { - const Component = Vue.extend(component); - let vm; + let wrapper; const erasedAt = '2016-11-07T11:11:16.525Z'; const timeago = getTimeago(); const formatedDate = timeago.format(erasedAt); + const createComponent = props => { + wrapper = mount(ErasedBlock, { + propsData: props, + sync: false, + attachToDocument: true, + }); + }; + afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('with job erased by user', () => { beforeEach(() => { - vm = mountComponent(Component, { + createComponent({ user: { username: 'root', web_url: 'gitlab.com/root', @@ -27,30 +34,30 @@ describe('Erased block', () => { }); it('renders username and link', () => { - expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('gitlab.com/root'); + expect(wrapper.find(GlLink).attributes('href')).toEqual('gitlab.com/root'); - expect(vm.$el.textContent).toContain('Job has been erased by'); - expect(vm.$el.textContent).toContain('root'); + expect(wrapper.text().trim()).toContain('Job has been erased by'); + expect(wrapper.text().trim()).toContain('root'); }); it('renders erasedAt', () => { - expect(vm.$el.textContent).toContain(formatedDate); + expect(wrapper.text().trim()).toContain(formatedDate); }); }); describe('with erased job', () => { beforeEach(() => { - vm = mountComponent(Component, { + createComponent({ erasedAt, }); }); it('renders username and link', () => { - expect(vm.$el.textContent).toContain('Job has been erased'); + expect(wrapper.text().trim()).toContain('Job has been erased'); }); it('renders erasedAt', () => { - expect(vm.$el.textContent).toContain(formatedDate); + expect(wrapper.text().trim()).toContain(formatedDate); }); }); }); diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index ee27789b6b9..fd75c9aa0cd 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -482,3 +482,27 @@ describe('secondsToMilliseconds', () => { expect(datetimeUtility.secondsToMilliseconds(123)).toBe(123000); }); }); + +describe('dayAfter', () => { + const date = new Date('2019-07-16T00:00:00.000Z'); + + it('returns the following date', () => { + const nextDay = datetimeUtility.dayAfter(date); + const expectedNextDate = new Date('2019-07-17T00:00:00.000Z'); + + expect(nextDay).toStrictEqual(expectedNextDate); + }); + + it('does not modifiy the original date', () => { + datetimeUtility.dayAfter(date); + expect(date).toStrictEqual(new Date('2019-07-16T00:00:00.000Z')); + }); +}); + +describe('secondsToDays', () => { + it('converts seconds to days correctly', () => { + expect(datetimeUtility.secondsToDays(0)).toBe(0); + expect(datetimeUtility.secondsToDays(90000)).toBe(1); + expect(datetimeUtility.secondsToDays(270000)).toBe(3); + }); +}); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index d4bc613ffea..467e0445a90 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -1,4 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; +import Tracking from '~/tracking'; import { TEST_HOST } from 'helpers/test_constants'; import testAction from 'helpers/vuex_action_helper'; import axios from '~/lib/utils/axios_utils'; @@ -226,12 +227,14 @@ describe('Monitoring store actions', () => { let state; const response = metricsDashboardResponse; beforeEach(() => { + jest.spyOn(Tracking, 'event'); dispatch = jest.fn(); state = storeState(); state.dashboardEndpoint = '/dashboard'; }); it('dispatches receive and success actions', done => { const params = {}; + document.body.dataset.page = 'projects:environments:metrics'; mock.onGet(state.dashboardEndpoint).reply(200, response); fetchDashboard( { @@ -246,6 +249,17 @@ describe('Monitoring store actions', () => { response, params, }); + }) + .then(() => { + expect(Tracking.event).toHaveBeenCalledWith( + document.body.dataset.page, + 'dashboard_fetch', + { + label: 'custom_metrics_dashboard', + property: 'count', + value: 0, + }, + ); done(); }) .catch(done.fail); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 45b99b71e06..475ea4f0f7d 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -38,6 +38,7 @@ describe('issue_comment_form component', () => { }, store, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index 91f9dab2530..3ccfea121b0 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -37,6 +37,8 @@ describe('DiscussionActions', () => { shouldShowJumpToNextDiscussion: true, ...props, }, + sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js index fd439ba46bd..ed173eacfab 100644 --- a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js +++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js @@ -7,6 +7,7 @@ describe('JumpToNextDiscussionButton', () => { beforeEach(() => { wrapper = shallowMount(JumpToNextDiscussionButton, { sync: false, + attachToDocument: true, }); }); diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js index 8881bedf3cc..b38cfa8fb4a 100644 --- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js +++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js @@ -37,6 +37,7 @@ describe('notes/components/discussion_keyboard_navigator', () => { isDiff ? NEXT_DIFF_ID : NEXT_ID; notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) => isDiff ? PREV_DIFF_ID : PREV_ID; + notes.getters.getDiscussion = () => id => ({ id }); storeOptions = { modules: { @@ -63,14 +64,18 @@ describe('notes/components/discussion_keyboard_navigator', () => { it('calls jumpToNextDiscussion when pressing `n`', () => { Mousetrap.trigger('n'); - expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedNextId); + expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith( + expect.objectContaining({ id: expectedNextId }), + ); expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId); }); it('calls jumpToPreviousDiscussion when pressing `p`', () => { Mousetrap.trigger('p'); - expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedPrevId); + expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith( + expect.objectContaining({ id: expectedPrevId }), + ); expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId); }); }); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index f77236b14bc..5ab26d742ca 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -36,6 +36,7 @@ describe('DiscussionNotes', () => { 'avatar-badge': '<span class="avatar-badge-slot-content" />', }, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js index b0f22bc63fb..1d03f0b655f 100644 --- a/spec/frontend/pipelines/test_reports/mock_data.js +++ b/spec/frontend/pipelines/test_reports/mock_data.js @@ -1,41 +1,6 @@ -import { formatTime } from '~/lib/utils/datetime_utility'; import { TestStatus } from '~/pipelines/constants'; -export const testCases = [ - { - classname: 'spec.test_spec', - execution_time: 0.000748, - name: 'Test#subtract when a is 1 and b is 2 raises an error', - stack_trace: null, - status: TestStatus.SUCCESS, - system_output: null, - }, - { - classname: 'spec.test_spec', - execution_time: 0.000064, - name: 'Test#subtract when a is 2 and b is 1 returns correct result', - stack_trace: null, - status: TestStatus.SUCCESS, - system_output: null, - }, - { - classname: 'spec.test_spec', - execution_time: 0.009292, - name: 'Test#sum when a is 1 and b is 2 returns summary', - stack_trace: null, - status: TestStatus.FAILED, - system_output: - "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'", - }, - { - classname: 'spec.test_spec', - execution_time: 0.00018, - name: 'Test#sum when a is 100 and b is 200 returns summary', - stack_trace: null, - status: TestStatus.FAILED, - system_output: - "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'", - }, +export default [ { classname: 'spec.test_spec', execution_time: 0, @@ -45,79 +10,3 @@ export const testCases = [ system_output: null, }, ]; - -export const testCasesFormatted = [ - { - ...testCases[2], - icon: 'status_failed_borderless', - formattedTime: formatTime(testCases[0].execution_time * 1000), - }, - { - ...testCases[3], - icon: 'status_failed_borderless', - formattedTime: formatTime(testCases[1].execution_time * 1000), - }, - { - ...testCases[4], - icon: 'status_skipped_borderless', - formattedTime: formatTime(testCases[2].execution_time * 1000), - }, - { - ...testCases[0], - icon: 'status_success_borderless', - formattedTime: formatTime(testCases[3].execution_time * 1000), - }, - { - ...testCases[1], - icon: 'status_success_borderless', - formattedTime: formatTime(testCases[4].execution_time * 1000), - }, -]; - -export const testSuites = [ - { - error_count: 0, - failed_count: 2, - name: 'rspec:osx', - skipped_count: 0, - success_count: 2, - test_cases: testCases, - total_count: 4, - total_time: 60, - }, - { - error_count: 0, - failed_count: 10, - name: 'rspec:osx', - skipped_count: 0, - success_count: 50, - test_cases: [], - total_count: 60, - total_time: 0.010284, - }, -]; - -export const testSuitesFormatted = testSuites.map(x => ({ - ...x, - formattedTime: formatTime(x.total_time * 1000), -})); - -export const testReports = { - error_count: 0, - failed_count: 2, - skipped_count: 0, - success_count: 2, - test_suites: testSuites, - total_count: 4, - total_time: 0.010284, -}; - -export const testReportsWithNoSuites = { - error_count: 0, - failed_count: 2, - skipped_count: 0, - success_count: 2, - test_suites: [], - total_count: 4, - total_time: 0.010284, -}; diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index c1721e12234..d7007eb7631 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -2,10 +2,10 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import * as actions from '~/pipelines/stores/test_reports/actions'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; +import { getJSONFixture } from 'helpers/fixtures'; import { TEST_HOST } from '../../../helpers/test_constants'; import testAction from '../../../helpers/vuex_action_helper'; import createFlash from '~/flash'; -import { testReports } from '../mock_data'; jest.mock('~/flash.js'); @@ -13,6 +13,8 @@ describe('Actions TestReports Store', () => { let mock; let state; + const testReports = getJSONFixture('pipelines/test_report.json'); + const endpoint = `${TEST_HOST}/test_reports.json`; const defaultState = { endpoint, diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js index e630a005409..cfd0ecdcb30 100644 --- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js @@ -1,9 +1,12 @@ import * as getters from '~/pipelines/stores/test_reports/getters'; -import { testReports, testSuitesFormatted, testCasesFormatted } from '../mock_data'; +import { iconForTestStatus } from '~/pipelines/stores/test_reports/utils'; +import { getJSONFixture } from 'helpers/fixtures'; describe('Getters TestReports Store', () => { let state; + const testReports = getJSONFixture('pipelines/test_report.json'); + const defaultState = { testReports, selectedSuite: testReports.test_suites[0], @@ -28,7 +31,13 @@ describe('Getters TestReports Store', () => { it('should return the test suites', () => { setupState(); - expect(getters.getTestSuites(state)).toEqual(testSuitesFormatted); + const suites = getters.getTestSuites(state); + const expected = testReports.test_suites.map(x => ({ + ...x, + formattedTime: '00:00:00', + })); + + expect(suites).toEqual(expected); }); it('should return an empty array when testReports is empty', () => { @@ -42,7 +51,14 @@ describe('Getters TestReports Store', () => { it('should return the test cases inside the suite', () => { setupState(); - expect(getters.getSuiteTests(state)).toEqual(testCasesFormatted); + const cases = getters.getSuiteTests(state); + const expected = testReports.test_suites[0].test_cases.map(x => ({ + ...x, + formattedTime: '00:00:00', + icon: iconForTestStatus(x.status), + })); + + expect(cases).toEqual(expected); }); it('should return an empty array when testReports is empty', () => { diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index ad5b7f91163..b891415f705 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -1,10 +1,12 @@ import * as types from '~/pipelines/stores/test_reports/mutation_types'; import mutations from '~/pipelines/stores/test_reports/mutations'; -import { testReports, testSuites } from '../mock_data'; +import { getJSONFixture } from 'helpers/fixtures'; describe('Mutations TestReports Store', () => { let mockState; + const testReports = getJSONFixture('pipelines/test_report.json'); + const defaultState = { endpoint: '', testReports: {}, @@ -27,7 +29,7 @@ describe('Mutations TestReports Store', () => { describe('set reports', () => { it('should set testReports', () => { - const expectedState = Object.assign({}, mockState, { testReports }); + const expectedState = { ...mockState, testReports }; mutations[types.SET_REPORTS](mockState, testReports); expect(mockState.testReports).toEqual(expectedState.testReports); @@ -36,10 +38,10 @@ describe('Mutations TestReports Store', () => { describe('set selected suite', () => { it('should set selectedSuite', () => { - const expectedState = Object.assign({}, mockState, { selectedSuite: testSuites[0] }); - mutations[types.SET_SELECTED_SUITE](mockState, testSuites[0]); + const selectedSuite = testReports.test_suites[0]; + mutations[types.SET_SELECTED_SUITE](mockState, selectedSuite); - expect(mockState.selectedSuite).toEqual(expectedState.selectedSuite); + expect(mockState.selectedSuite).toEqual(selectedSuite); }); }); diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js index 4d6422745a9..033c3300098 100644 --- a/spec/frontend/pipelines/test_reports/test_reports_spec.js +++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js @@ -1,13 +1,15 @@ import Vuex from 'vuex'; import TestReports from '~/pipelines/components/test_reports/test_reports.vue'; import { shallowMount } from '@vue/test-utils'; -import { testReports } from './mock_data'; import * as actions from '~/pipelines/stores/test_reports/actions'; +import { getJSONFixture } from 'helpers/fixtures'; describe('Test reports app', () => { let wrapper; let store; + const testReports = getJSONFixture('pipelines/test_report.json'); + const loadingSpinner = () => wrapper.find('.js-loading-spinner'); const testsDetail = () => wrapper.find('.js-tests-detail'); const noTestsToShow = () => wrapper.find('.js-no-tests-to-show'); diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js index b4305719ea8..bc5d8647d6a 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -3,18 +3,26 @@ import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue import * as getters from '~/pipelines/stores/test_reports/getters'; import { TestStatus } from '~/pipelines/constants'; import { shallowMount } from '@vue/test-utils'; -import { testSuites, testCases } from './mock_data'; +import { getJSONFixture } from 'helpers/fixtures'; +import skippedTestCases from './mock_data'; describe('Test reports suite table', () => { let wrapper; let store; + const { + test_suites: [testSuite], + } = getJSONFixture('pipelines/test_report.json'); + + testSuite.test_cases = [...testSuite.test_cases, ...skippedTestCases]; + const testCases = testSuite.test_cases; + const noCasesMessage = () => wrapper.find('.js-no-test-cases'); const allCaseRows = () => wrapper.findAll('.js-case-row'); const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index); const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`); - const createComponent = (suite = testSuites[0]) => { + const createComponent = (suite = testSuite) => { store = new Vuex.Store({ state: { selectedSuite: suite, diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index 19a7755dbdc..864c7b6f4de 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -1,10 +1,14 @@ import Summary from '~/pipelines/components/test_reports/test_summary.vue'; import { mount } from '@vue/test-utils'; -import { testSuites } from './mock_data'; +import { getJSONFixture } from 'helpers/fixtures'; describe('Test reports summary', () => { let wrapper; + const { + test_suites: [testSuite], + } = getJSONFixture('pipelines/test_report.json'); + const backButton = () => wrapper.find('.js-back-button'); const totalTests = () => wrapper.find('.js-total-tests'); const failedTests = () => wrapper.find('.js-failed-tests'); @@ -13,7 +17,7 @@ describe('Test reports summary', () => { const duration = () => wrapper.find('.js-duration'); const defaultProps = { - report: testSuites[0], + report: testSuite, showBack: false, }; @@ -72,7 +76,7 @@ describe('Test reports summary', () => { }); it('displays the correctly formatted duration', () => { - expect(duration().text()).toBe('00:01:00'); + expect(duration().text()).toBe('00:00:00'); }); }); }); diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js index e7599d5cdbc..7d06d96fe75 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js @@ -2,7 +2,7 @@ import Vuex from 'vuex'; import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue'; import * as getters from '~/pipelines/stores/test_reports/getters'; import { mount, createLocalVue } from '@vue/test-utils'; -import { testReports, testReportsWithNoSuites } from './mock_data'; +import { getJSONFixture } from 'helpers/fixtures'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -11,6 +11,8 @@ describe('Test reports summary table', () => { let wrapper; let store; + const testReports = getJSONFixture('pipelines/test_report.json'); + const allSuitesRows = () => wrapper.findAll('.js-suite-row'); const noSuitesToShow = () => wrapper.find('.js-no-tests-suites'); @@ -44,7 +46,7 @@ describe('Test reports summary table', () => { describe('when there are no test suites', () => { beforeEach(() => { - createComponent({ testReportsWithNoSuites }); + createComponent({ test_suites: [] }); }); it('displays the no suites to show message', () => { diff --git a/spec/frontend/releases/list/components/evidence_block_spec.js b/spec/frontend/releases/list/components/evidence_block_spec.js new file mode 100644 index 00000000000..e8a3eace216 --- /dev/null +++ b/spec/frontend/releases/list/components/evidence_block_spec.js @@ -0,0 +1,77 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import { truncateSha } from '~/lib/utils/text_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import { release } from '../../mock_data'; +import EvidenceBlock from '~/releases/list/components/evidence_block.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +describe('Evidence Block', () => { + let wrapper; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = mount(localVue.extend(EvidenceBlock), { + localVue, + ...options, + }); + }; + + beforeEach(() => { + factory({ + propsData: { + release, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the evidence icon', () => { + expect(wrapper.find(Icon).props('name')).toBe('review-list'); + }); + + it('renders the title for the dowload link', () => { + expect(wrapper.find(GlLink).text()).toBe(`${release.tag_name}-evidence.json`); + }); + + it('renders the correct hover text for the download', () => { + expect(wrapper.find(GlLink).attributes('data-original-title')).toBe('Download evidence JSON'); + }); + + it('renders the correct file link for download', () => { + expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tag_name}-evidence.json`); + }); + + describe('sha text', () => { + it('renders the short sha initially', () => { + expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidence_sha)); + }); + + it('renders the long sha after expansion', () => { + wrapper.find('.js-text-expander-prepend').trigger('click'); + expect(wrapper.find('.js-expanded').text()).toBe(release.evidence_sha); + }); + }); + + describe('copy to clipboard button', () => { + it('renders button', () => { + expect(wrapper.find(ClipboardButton).exists()).toBe(true); + }); + + it('renders the correct hover text', () => { + expect(wrapper.find(ClipboardButton).attributes('data-original-title')).toBe( + 'Copy commit SHA', + ); + }); + + it('copies the sha', () => { + expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe( + release.evidence_sha, + ); + }); + }); +}); diff --git a/spec/frontend/releases/list/components/release_block_spec.js b/spec/frontend/releases/list/components/release_block_spec.js index b63ef068d8e..056835bdaf2 100644 --- a/spec/frontend/releases/list/components/release_block_spec.js +++ b/spec/frontend/releases/list/components/release_block_spec.js @@ -1,4 +1,5 @@ import { mount } from '@vue/test-utils'; +import EvidenceBlock from '~/releases/list/components/evidence_block.vue'; import ReleaseBlock from '~/releases/list/components/release_block.vue'; import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -29,7 +30,6 @@ describe('Release block', () => { }, provide: { glFeatures: { - releaseEditPage: true, releaseIssueSummary: true, ...featureFlags, }, @@ -179,11 +179,6 @@ describe('Release block', () => { }); }); - it('does not render an edit button if the releaseEditPage feature flag is disabled', () => - factory(releaseClone, { releaseEditPage: false }).then(() => { - expect(editButton().exists()).toBe(false); - })); - it('does not render the milestone list if no milestones are associated to the release', () => { delete releaseClone.milestones; @@ -220,6 +215,26 @@ describe('Release block', () => { }); }); + describe('evidence block', () => { + it('renders the evidence block when the evidence is available and the feature flag is true', () => + factory(releaseClone, { releaseEvidenceCollection: true }).then(() => + expect(wrapper.find(EvidenceBlock).exists()).toBe(true), + )); + + it('does not render the evidence block when the evidence is available but the feature flag is false', () => + factory(releaseClone, { releaseEvidenceCollection: true }).then(() => + expect(wrapper.find(EvidenceBlock).exists()).toBe(true), + )); + + it('does not render the evidence block when there is no evidence', () => { + releaseClone.evidence_sha = null; + + return factory(releaseClone).then(() => { + expect(wrapper.find(EvidenceBlock).exists()).toBe(false); + }); + }); + }); + describe('anchor scrolling', () => { beforeEach(() => { scrollToElement.mockClear(); diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js index 61d95b86b1c..e1830b494bc 100644 --- a/spec/frontend/releases/mock_data.js +++ b/spec/frontend/releases/mock_data.js @@ -35,6 +35,7 @@ export const release = { description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>', created_at: '2019-08-26T17:54:04.952Z', released_at: '2019-08-26T17:54:04.807Z', + evidence_sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d', author: { id: 1, name: 'Administrator', @@ -62,6 +63,8 @@ export const release = { milestones, assets: { count: 5, + evidence_file_path: + 'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidence.json', sources: [ { format: 'zip', diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js index 9199c726680..4271a038680 100644 --- a/spec/frontend/repository/log_tree_spec.js +++ b/spec/frontend/repository/log_tree_spec.js @@ -21,11 +21,18 @@ describe('resolveCommit', () => { entry: { name: 'index.js', type: 'blob' }, resolve: jest.fn(), }; - const commits = [{ fileName: 'index.js', type: 'blob' }]; - - resolveCommit(commits, resolver); - - expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', type: 'blob' }); + const commits = [ + { fileName: 'index.js', filePath: '/index.js', type: 'blob' }, + { fileName: 'index.js', filePath: '/app/assets/index.js', type: 'blob' }, + ]; + + resolveCommit(commits, '', resolver); + + expect(resolver.resolve).toHaveBeenCalledWith({ + fileName: 'index.js', + filePath: '/index.js', + type: 'blob', + }); }); }); @@ -84,6 +91,7 @@ describe('fetchLogsTree', () => { commitPath: 'https://test.com', committedDate: '2019-01-01', fileName: 'index.js', + filePath: '/index.js', message: 'testing message', sha: '123', type: 'blob', @@ -101,6 +109,7 @@ describe('fetchLogsTree', () => { commitPath: 'https://test.com', committedDate: '2019-01-01', fileName: 'index.js', + filePath: '/index.js', message: 'testing message', sha: '123', type: 'blob', diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js index 2d75358106c..e7cc28178bf 100644 --- a/spec/frontend/repository/utils/commit_spec.js +++ b/spec/frontend/repository/utils/commit_spec.js @@ -15,13 +15,14 @@ const mockData = [ describe('normalizeData', () => { it('normalizes data into LogTreeCommit object', () => { - expect(normalizeData(mockData)).toEqual([ + expect(normalizeData(mockData, '')).toEqual([ { sha: '123', message: 'testing message', committedDate: '2019-01-01', commitPath: 'https://test.com', fileName: 'index.js', + filePath: '/index.js', type: 'blob', __typename: 'LogTreeCommit', }, diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js index 678d444904d..bf98a9e1a4d 100644 --- a/spec/frontend/repository/utils/dom_spec.js +++ b/spec/frontend/repository/utils/dom_spec.js @@ -1,5 +1,5 @@ import { setHTMLFixture } from '../../helpers/fixtures'; -import { updateElementsVisibility } from '~/repository/utils/dom'; +import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom'; describe('updateElementsVisibility', () => { it('adds hidden class', () => { @@ -18,3 +18,13 @@ describe('updateElementsVisibility', () => { expect(document.querySelector('.js-test').classList).not.toContain('hidden'); }); }); + +describe('updateFormAction', () => { + it('updates form action', () => { + setHTMLFixture('<form class="js-test" action="/"></form>'); + + updateFormAction('.js-test', '/gitlab/create', '/test'); + + expect(document.querySelector('.js-test').action).toBe('http://localhost/gitlab/create/test'); + }); +}); diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap new file mode 100644 index 00000000000..cf71aefebe8 --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Expand button on click when short text is provided renders button after text 1`] = `"<span><button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-prepend text-expander btn-blank btn-secondary\\" style=\\"display: none;\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button> <!----> <span><p>Expanded!</p></span> <button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-append text-expander btn-blank btn-secondary\\" style=\\"\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button></span>"`; + +exports[`Expand button when short text is provided renders button before text 1`] = `"<span><button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-prepend text-expander btn-blank btn-secondary\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button> <span><p>Short</p></span> <!----> <button aria-label=\\"Click to expand text\\" type=\\"button\\" class=\\"btn js-text-expander-append text-expander btn-blank btn-secondary\\" style=\\"display: none;\\"><svg aria-hidden=\\"true\\" class=\\"s12 ic-ellipsis_h\\"><use xlink:href=\\"#ellipsis_h\\"></use></svg></button></span>"`; diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js index d5861b18318..2fabbe3d0f6 100644 --- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -19,6 +19,7 @@ describe('Changed file icon', () => { ...props, }, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js index 77d8e00cf00..67262eec0a5 100644 --- a/spec/frontend/vue_shared/components/commit_spec.js +++ b/spec/frontend/vue_shared/components/commit_spec.js @@ -13,6 +13,7 @@ describe('Commit component', () => { wrapper = shallowMount(CommitComponent, { propsData, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a501e6695d5 --- /dev/null +++ b/spec/frontend/vue_shared/components/expand_button_spec.js @@ -0,0 +1,188 @@ +import Vue from 'vue'; +import { mount, createLocalVue } from '@vue/test-utils'; +import ExpandButton from '~/vue_shared/components/expand_button.vue'; + +const text = { + expanded: 'Expanded!', + short: 'Short', +}; + +describe('Expand button', () => { + let wrapper; + + const expanderPrependEl = () => wrapper.find('.js-text-expander-prepend'); + const expanderAppendEl = () => wrapper.find('.js-text-expander-append'); + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = mount(localVue.extend(ExpandButton), { + localVue, + ...options, + }); + }; + + beforeEach(() => { + factory({ + slots: { + expanded: `<p>${text.expanded}</p>`, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the prepended collapse button', () => { + expect(expanderPrependEl().isVisible()).toBe(true); + expect(expanderAppendEl().isVisible()).toBe(false); + }); + + it('renders no text when short text is not provided', () => { + expect(wrapper.find(ExpandButton).text()).toBe(''); + }); + + it('does not render expanded text', () => { + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).not.toBe(text.short); + }); + + describe('when short text is provided', () => { + beforeEach(() => { + factory({ + slots: { + expanded: `<p>${text.expanded}</p>`, + short: `<p>${text.short}</p>`, + }, + }); + }); + + it('renders short text', () => { + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).toBe(text.short); + }); + + it('renders button before text', () => { + expect(expanderPrependEl().isVisible()).toBe(true); + expect(expanderAppendEl().isVisible()).toBe(false); + expect(wrapper.find(ExpandButton).html()).toMatchSnapshot(); + }); + }); + + describe('on click', () => { + beforeEach(done => { + expanderPrependEl().trigger('click'); + Vue.nextTick(done); + }); + + afterEach(() => { + expanderAppendEl().trigger('click'); + }); + + it('renders only the append collapse button', () => { + expect(expanderAppendEl().isVisible()).toBe(true); + expect(expanderPrependEl().isVisible()).toBe(false); + }); + + it('renders the expanded text', () => { + expect(wrapper.find(ExpandButton).text()).toContain(text.expanded); + }); + + describe('when short text is provided', () => { + beforeEach(done => { + factory({ + slots: { + expanded: `<p>${text.expanded}</p>`, + short: `<p>${text.short}</p>`, + }, + }); + + expanderPrependEl().trigger('click'); + Vue.nextTick(done); + }); + + it('only renders expanded text', () => { + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).toBe(text.expanded); + }); + + it('renders button after text', () => { + expect(expanderPrependEl().isVisible()).toBe(false); + expect(expanderAppendEl().isVisible()).toBe(true); + expect(wrapper.find(ExpandButton).html()).toMatchSnapshot(); + }); + }); + }); + + describe('append button', () => { + beforeEach(done => { + expanderPrependEl().trigger('click'); + Vue.nextTick(done); + }); + + it('clicking hides itself and shows prepend', () => { + expect(expanderAppendEl().isVisible()).toBe(true); + expanderAppendEl().trigger('click'); + expect(expanderPrependEl().isVisible()).toBe(true); + }); + + it('clicking hides expanded text', () => { + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).toBe(text.expanded); + expanderAppendEl().trigger('click'); + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).not.toBe(text.expanded); + }); + + describe('when short text is provided', () => { + beforeEach(done => { + factory({ + slots: { + expanded: `<p>${text.expanded}</p>`, + short: `<p>${text.short}</p>`, + }, + }); + + expanderPrependEl().trigger('click'); + Vue.nextTick(done); + }); + + it('clicking reveals short text', () => { + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).toBe(text.expanded); + expanderAppendEl().trigger('click'); + expect( + wrapper + .find(ExpandButton) + .text() + .trim(), + ).toBe(text.short); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js index 9e6b5286899..dcae2f12833 100644 --- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js @@ -18,6 +18,7 @@ describe('IssueAssigneesComponent', () => { ...props, }, sync: false, + attachToDocument: true, }); vm = wrapper.vm; // eslint-disable-line }; diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js index 2e93ec412b9..4a66330ac30 100644 --- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js +++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js @@ -1,18 +1,20 @@ import Vue from 'vue'; -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import { mockMilestone } from '../../../../javascripts/boards/mock_data'; const createComponent = (milestone = mockMilestone) => { const Component = Vue.extend(IssueMilestone); - return mount(Component, { + return shallowMount(Component, { propsData: { milestone, }, sync: false, + attachToDocument: true, }); }; @@ -156,7 +158,7 @@ describe('IssueMilestoneComponent', () => { }); it('renders milestone icon', () => { - expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock'); + expect(wrapper.find(Icon).props('name')).toBe('clock'); }); it('renders milestone title', () => { diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js index b85e2673624..cc9b569793d 100644 --- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js +++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js @@ -35,6 +35,7 @@ describe('RelatedIssuableItem', () => { localVue, slots, sync: false, + attachToDocument: true, propsData: props, }); }); diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js new file mode 100644 index 00000000000..b006f72b8ee --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/field_spec.js @@ -0,0 +1,179 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import fieldComponent from '~/vue_shared/components/markdown/field.vue'; +import { TEST_HOST } from 'spec/test_constants'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import $ from 'jquery'; + +const markdownPreviewPath = `${TEST_HOST}/preview`; +const markdownDocsPath = `${TEST_HOST}/docs`; + +function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) { + expect(writeLink.element.parentNode.classList.contains('active')).toEqual(isWrite); + expect(previewLink.element.parentNode.classList.contains('active')).toEqual(!isWrite); + expect(wrapper.find('.md-preview-holder').element.style.display).toEqual(isWrite ? 'none' : ''); +} + +function createComponent() { + const wrapper = mount(fieldComponent, { + propsData: { + markdownDocsPath, + markdownPreviewPath, + }, + slots: { + textarea: '<textarea>testing\n123</textarea>', + }, + template: ` + <field-component + markdown-preview-path="${markdownPreviewPath}" + markdown-docs-path="${markdownDocsPath}" + > + <textarea + slot="textarea" + v-model="text"> + <slot>this is a test</slot> + </textarea> + </field-component> + `, + sync: false, + }); + return wrapper; +} + +const getPreviewLink = wrapper => wrapper.find('.nav-links .js-preview-link'); +const getWriteLink = wrapper => wrapper.find('.nav-links .js-write-link'); +const getMarkdownButton = wrapper => wrapper.find('.js-md'); +const getAllMarkdownButtons = wrapper => wrapper.findAll('.js-md'); + +describe('Markdown field component', () => { + let axiosMock; + const localVue = createLocalVue(); + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + describe('mounted', () => { + let wrapper; + const previewHTML = '<p>markdown preview</p>'; + let previewLink; + let writeLink; + + it('renders textarea inside backdrop', () => { + wrapper = createComponent(); + expect(wrapper.find('.zen-backdrop textarea').element).not.toBeNull(); + }); + + describe('markdown preview', () => { + beforeEach(() => { + axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML }); + }); + + it('sets preview link as active', () => { + wrapper = createComponent(); + previewLink = getPreviewLink(wrapper); + previewLink.trigger('click'); + + return localVue.nextTick().then(() => { + expect(previewLink.element.parentNode.classList.contains('active')).toBeTruthy(); + }); + }); + + it('shows preview loading text', () => { + wrapper = createComponent(); + previewLink = getPreviewLink(wrapper); + previewLink.trigger('click'); + + localVue.nextTick(() => { + expect(wrapper.find('.md-preview-holder').element.textContent.trim()).toContain( + 'Loading…', + ); + }); + }); + + it('renders markdown preview', () => { + wrapper = createComponent(); + previewLink = getPreviewLink(wrapper); + previewLink.trigger('click'); + + setTimeout(() => { + expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML); + }); + }); + + it('renders GFM with jQuery', () => { + wrapper = createComponent(); + previewLink = getPreviewLink(wrapper); + jest.spyOn($.fn, 'renderGFM'); + + previewLink.trigger('click'); + + setTimeout(() => { + expect($.fn.renderGFM).toHaveBeenCalled(); + }, 0); + }); + + it('clicking already active write or preview link does nothing', () => { + wrapper = createComponent(); + writeLink = getWriteLink(wrapper); + previewLink = getPreviewLink(wrapper); + + writeLink.trigger('click'); + return localVue + .nextTick() + .then(() => assertMarkdownTabs(true, writeLink, previewLink, wrapper)) + .then(() => writeLink.trigger('click')) + .then(() => localVue.nextTick()) + .then(() => assertMarkdownTabs(true, writeLink, previewLink, wrapper)) + .then(() => previewLink.trigger('click')) + .then(() => localVue.nextTick()) + .then(() => assertMarkdownTabs(false, writeLink, previewLink, wrapper)) + .then(() => previewLink.trigger('click')) + .then(() => localVue.nextTick()) + .then(() => assertMarkdownTabs(false, writeLink, previewLink, wrapper)); + }); + }); + + describe('markdown buttons', () => { + it('converts single words', () => { + wrapper = createComponent(); + const textarea = wrapper.find('textarea').element; + textarea.setSelectionRange(0, 7); + const markdownButton = getMarkdownButton(wrapper); + markdownButton.trigger('click'); + + localVue.nextTick(() => { + expect(textarea.value).toContain('**testing**'); + }); + }); + + it('converts a line', () => { + wrapper = createComponent(); + const textarea = wrapper.find('textarea').element; + textarea.setSelectionRange(0, 0); + const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5]; + markdownButton.trigger('click'); + + localVue.nextTick(() => { + expect(textarea.value).toContain('* testing'); + }); + }); + + it('converts multiple lines', () => { + wrapper = createComponent(); + const textarea = wrapper.find('textarea').element; + textarea.setSelectionRange(0, 50); + const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5]; + markdownButton.trigger('click'); + + localVue.nextTick(() => { + expect(textarea.value).toContain('* testing\n* 123'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js index 9e0b98ecef9..71f9b5e3244 100644 --- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -21,6 +21,7 @@ describe('Suggestion Diff component', () => { }, localVue, sync: false, + attachToDocument: true, }); }; diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index d8c55bee8e0..3c71cb16bd5 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -16,6 +16,8 @@ describe('modal copy button', () => { text: 'copy me', title: 'Copy this value', }, + attachToDocument: true, + sync: false, }); }); diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js index 31ac362d35f..92b48ce9974 100644 --- a/spec/frontend/vue_shared/components/paginated_list_spec.js +++ b/spec/frontend/vue_shared/components/paginated_list_spec.js @@ -26,6 +26,8 @@ describe('Pagination links component', () => { list: [{ id: 'foo' }, { id: 'bar' }], props, }, + attachToDocument: true, + sync: false, }); [glPaginatedList] = wrapper.vm.$children; diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js index 6aee616c324..91865dcea0a 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js @@ -1,22 +1,20 @@ import Vue from 'vue'; import LabelsSelect from '~/labels_select'; -import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; +import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import { mockConfig, mockLabels, } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; -const createComponent = (config = mockConfig) => { - const Component = Vue.extend(baseComponent); - - return mount(Component, { +const createComponent = (config = mockConfig) => + shallowMount(BaseComponent, { propsData: config, sync: false, + attachToDocument: true, }); -}; describe('BaseComponent', () => { let wrapper; diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js index 536bb57b946..f1f231c1a29 100644 --- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js @@ -1,44 +1,40 @@ -import Vue from 'vue'; -import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; describe('Time ago with tooltip component', () => { - let TimeagoTooltip; let vm; - beforeEach(() => { - TimeagoTooltip = Vue.extend(timeagoTooltip); - }); + const buildVm = (propsData = {}) => { + vm = shallowMount(TimeAgoTooltip, { + attachToDocument: true, + sync: false, + propsData, + localVue: createLocalVue(), + }); + }; + const timestamp = '2017-05-08T14:57:39.781Z'; afterEach(() => { - vm.$destroy(); + vm.destroy(); }); it('should render timeago with a bootstrap tooltip', () => { - vm = new TimeagoTooltip({ - propsData: { - time: '2017-05-08T14:57:39.781Z', - }, - }).$mount(); - - expect(vm.$el.tagName).toEqual('TIME'); - expect(vm.$el.getAttribute('data-original-title')).toEqual( - formatDate('2017-05-08T14:57:39.781Z'), - ); - + buildVm({ + time: timestamp, + }); const timeago = getTimeago(); - expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); + expect(vm.attributes('data-original-title')).toEqual(formatDate(timestamp)); + expect(vm.text()).toEqual(timeago.format(timestamp)); }); it('should render provided html class', () => { - vm = new TimeagoTooltip({ - propsData: { - time: '2017-05-08T14:57:39.781Z', - cssClass: 'foo', - }, - }).$mount(); + buildVm({ + time: timestamp, + cssClass: 'foo', + }); - expect(vm.$el.classList.contains('foo')).toEqual(true); + expect(vm.classes()).toContain('foo'); }); }); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index fc2eb6329b0..2750b54521a 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -1,5 +1,7 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; -import { mount } from '@vue/test-utils'; +import { GlSkeletonLoading } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { shallowMount } from '@vue/test-utils'; const DEFAULT_PROPS = { loaded: true, @@ -29,7 +31,7 @@ describe('User Popover Component', () => { describe('Empty', () => { beforeEach(() => { - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { target: document.querySelector('.js-user-link'), user: { @@ -41,18 +43,19 @@ describe('User Popover Component', () => { status: null, }, }, + attachToDocument: true, sync: false, }); }); it('should return skeleton loaders', () => { - expect(wrapper.findAll('.animation-container').length).toBe(4); + expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true); }); }); describe('basic data', () => { it('should show basic fields', () => { - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), @@ -66,9 +69,9 @@ describe('User Popover Component', () => { }); it('shows icon for location', () => { - const iconEl = wrapper.find('.js-location svg'); + const iconEl = wrapper.find(Icon); - expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('location'); + expect(iconEl.props('name')).toEqual('location'); }); }); @@ -77,7 +80,7 @@ describe('User Popover Component', () => { const testProps = Object.assign({}, DEFAULT_PROPS); testProps.user.bio = 'Engineer'; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...testProps, target: document.querySelector('.js-user-link'), @@ -92,7 +95,7 @@ describe('User Popover Component', () => { const testProps = Object.assign({}, DEFAULT_PROPS); testProps.user.organization = 'GitLab'; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...testProps, target: document.querySelector('.js-user-link'), @@ -108,7 +111,7 @@ describe('User Popover Component', () => { testProps.user.bio = 'Engineer'; testProps.user.organization = 'GitLab'; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), @@ -125,7 +128,7 @@ describe('User Popover Component', () => { testProps.user.bio = 'Manager & Team Lead'; testProps.user.organization = 'Me & my <funky> Company'; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), @@ -138,15 +141,13 @@ describe('User Popover Component', () => { }); it('shows icon for bio', () => { - const iconEl = wrapper.find('.js-bio svg'); - - expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('profile'); + expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual( + 1, + ); }); it('shows icon for organization', () => { - const iconEl = wrapper.find('.js-organization svg'); - - expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('work'); + expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1); }); }); @@ -155,7 +156,7 @@ describe('User Popover Component', () => { const testProps = Object.assign({}, DEFAULT_PROPS); testProps.user.status = { message_html: 'Hello World' }; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), @@ -170,7 +171,7 @@ describe('User Popover Component', () => { const testProps = Object.assign({}, DEFAULT_PROPS); testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; - wrapper = mount(UserPopover, { + wrapper = shallowMount(UserPopover, { propsData: { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), diff --git a/spec/graphql/mutations/issues/set_due_date_spec.rb b/spec/graphql/mutations/issues/set_due_date_spec.rb new file mode 100644 index 00000000000..9a1f0925fe3 --- /dev/null +++ b/spec/graphql/mutations/issues/set_due_date_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mutations::Issues::SetDueDate do + let(:issue) { create(:issue) } + let(:user) { create(:user) } + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) } + + describe '#resolve' do + let(:due_date) { 2.days.since } + let(:mutated_issue) { subject[:issue] } + subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, due_date: due_date) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the issue' do + before do + issue.project.add_developer(user) + end + + it 'returns the issue with updated due date' do + expect(mutated_issue).to eq(issue) + expect(mutated_issue.due_date).to eq(Date.today + 2.days) + expect(subject[:errors]).to be_empty + end + + context 'when passing incorrect due date value' do + let(:due_date) { 'test' } + + it 'does not update due date' do + expect(mutated_issue.due_date).to eq(issue.due_date) + end + end + end + end +end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 695d1520897..6bd567eab57 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -26,6 +26,31 @@ describe MergeRequestsHelper do end end + describe '#state_name_with_icon' do + using RSpec::Parameterized::TableSyntax + + let(:merge_request) { MergeRequest.new } + + where(:state, :expected_name, :expected_icon) do + :merged? | 'Merged' | 'git-merge' + :closed? | 'Closed' | 'close' + :opened? | 'Open' | 'issue-open-m' + end + + with_them do + before do + allow(merge_request).to receive(state).and_return(true) + end + + it 'returns name and icon' do + name, icon = helper.state_name_with_icon(merge_request) + + expect(name).to eq(expected_name) + expect(icon).to eq(expected_icon) + end + end + end + describe '#format_mr_branch_names' do describe 'within the same project' do let(:merge_request) { create(:merge_request) } diff --git a/spec/javascripts/badges/components/badge_form_spec.js b/spec/javascripts/badges/components/badge_form_spec.js index 651ac3ba3f9..b40afc40ca0 100644 --- a/spec/javascripts/badges/components/badge_form_spec.js +++ b/spec/javascripts/badges/components/badge_form_spec.js @@ -51,13 +51,14 @@ describe('BadgeForm component', () => { }); const sharedSubmitTests = submitAction => { + const nameSelector = '#badge-name'; const imageUrlSelector = '#badge-image-url'; const findImageUrlElement = () => vm.$el.querySelector(imageUrlSelector); const linkUrlSelector = '#badge-link-url'; const findLinkUrlElement = () => vm.$el.querySelector(linkUrlSelector); - const setValue = (inputElementSelector, url) => { + const setValue = (inputElementSelector, value) => { const inputElement = vm.$el.querySelector(inputElementSelector); - inputElement.value = url; + inputElement.value = value; inputElement.dispatchEvent(new Event('input')); }; const submitForm = () => { @@ -82,6 +83,7 @@ describe('BadgeForm component', () => { isSaving: false, }); + setValue(nameSelector, 'TestBadge'); setValue(linkUrlSelector, `${TEST_HOST}/link/url`); setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`); }); diff --git a/spec/javascripts/badges/components/badge_list_row_spec.js b/spec/javascripts/badges/components/badge_list_row_spec.js index a5b47cc5f32..34a92cc2018 100644 --- a/spec/javascripts/badges/components/badge_list_row_spec.js +++ b/spec/javascripts/badges/components/badge_list_row_spec.js @@ -39,6 +39,10 @@ describe('BadgeListRow component', () => { expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl); }); + it('renders the badge name', () => { + expect(vm.$el).toContainText(badge.name); + }); + it('renders the badge link', () => { expect(vm.$el).toContainText(badge.linkUrl); }); diff --git a/spec/javascripts/badges/dummy_badge.js b/spec/javascripts/badges/dummy_badge.js index f0cdaddbd33..ffc21c960b9 100644 --- a/spec/javascripts/badges/dummy_badge.js +++ b/spec/javascripts/badges/dummy_badge.js @@ -6,6 +6,7 @@ export const createDummyBadge = () => { const id = _.uniqueId(); return { id, + name: 'TestBadge', imageUrl: `${TEST_HOST}/badges/${id}/image/url`, isDeleting: false, linkUrl: `${TEST_HOST}/badges/${id}/link/url`, @@ -16,6 +17,7 @@ export const createDummyBadge = () => { }; export const createDummyBadgeResponse = () => ({ + name: 'TestBadge', image_url: `${TEST_HOST}/badge/image/url`, link_url: `${TEST_HOST}/badge/link/url`, kind: PROJECT_BADGE, diff --git a/spec/javascripts/badges/store/actions_spec.js b/spec/javascripts/badges/store/actions_spec.js index e8d5f8c3aac..a4cdff8129d 100644 --- a/spec/javascripts/badges/store/actions_spec.js +++ b/spec/javascripts/badges/store/actions_spec.js @@ -90,6 +90,7 @@ describe('Badges store actions', () => { endpointMock.replyOnce(req => { expect(req.data).toBe( JSON.stringify({ + name: 'TestBadge', image_url: badgeInAddForm.imageUrl, link_url: badgeInAddForm.linkUrl, }), @@ -114,6 +115,7 @@ describe('Badges store actions', () => { endpointMock.replyOnce(req => { expect(req.data).toBe( JSON.stringify({ + name: 'TestBadge', image_url: badgeInAddForm.imageUrl, link_url: badgeInAddForm.linkUrl, }), @@ -526,6 +528,7 @@ describe('Badges store actions', () => { endpointMock.replyOnce(req => { expect(req.data).toBe( JSON.stringify({ + name: 'TestBadge', image_url: badgeInEditForm.imageUrl, link_url: badgeInEditForm.linkUrl, }), @@ -550,6 +553,7 @@ describe('Badges store actions', () => { endpointMock.replyOnce(req => { expect(req.data).toBe( JSON.stringify({ + name: 'TestBadge', image_url: badgeInEditForm.imageUrl, link_url: badgeInEditForm.linkUrl, }), diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index fdf8bcee756..52f7674a7b3 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -34,6 +34,8 @@ describe('diffs/components/app', () => { localVue, propsData: { endpoint: `${TEST_HOST}/diff/endpoint`, + endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`, + endpointBatch: `${TEST_HOST}/diff/endpointBatch`, projectPath: 'namespace/project', currentUser: {}, changesEmptyStateIllustration: '', @@ -42,6 +44,11 @@ describe('diffs/components/app', () => { ...props, }, store, + methods: { + isLatestVersion() { + return true; + }, + }, }); } @@ -59,6 +66,58 @@ describe('diffs/components/app', () => { wrapper.destroy(); }); + describe('fetch diff methods', () => { + beforeEach(() => { + spyOn(window, 'requestIdleCallback').and.callFake(fn => fn()); + createComponent(); + spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(() => Promise.resolve()); + spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(() => Promise.resolve()); + spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(() => Promise.resolve()); + spyOn(wrapper.vm, 'setDiscussions'); + spyOn(wrapper.vm, 'startRenderDiffsQueue'); + }); + + it('calls fetchDiffFiles if diffsBatchLoad is not enabled', () => { + wrapper.vm.glFeatures.diffsBatchLoad = false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled(); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.setDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + }); + + it('calls fetchDiffFiles if diffsBatchLoad is enabled, and not latest version', () => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.isLatestVersion = () => false; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled(); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.setDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled(); + }); + }); + + it('calls batch methods if diffsBatchLoad is enabled, and latest version', () => { + wrapper.vm.glFeatures.diffsBatchLoad = true; + wrapper.vm.fetchData(false); + + expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled(); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.setDiscussions).toHaveBeenCalled(); + expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled(); + expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled(); + }); + }); + }); + it('adds container-limiting classes when showFileTree is false with inline diffs', () => { createComponent({}, ({ state }) => { state.diffs.showTreeList = false; @@ -93,6 +152,14 @@ describe('diffs/components/app', () => { expect(wrapper.contains(GlLoadingIcon)).toBe(true); }); + it('displays loading icon on batch loading', () => { + createComponent({}, ({ state }) => { + state.diffs.isBatchLoading = true; + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + it('displays diffs container when not loading', () => { createComponent(); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 874891fcc6e..3235febe0dc 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -8,6 +8,8 @@ import { import actions, { setBaseConfig, fetchDiffFiles, + fetchDiffFilesBatch, + fetchDiffFilesMeta, assignDiscussionsToDiff, removeDiscussionsFromDiff, startRenderDiffsQueue, @@ -68,18 +70,41 @@ describe('DiffsStoreActions', () => { describe('setBaseConfig', () => { it('should set given endpoint and project path', done => { const endpoint = '/diffs/set/endpoint'; + const endpointMetadata = '/diffs/set/endpoint/metadata'; + const endpointBatch = '/diffs/set/endpoint/batch'; const projectPath = '/root/project'; const dismissEndpoint = '/-/user_callouts'; const showSuggestPopover = false; testAction( setBaseConfig, - { endpoint, projectPath, dismissEndpoint, showSuggestPopover }, - { endpoint: '', projectPath: '', dismissEndpoint: '', showSuggestPopover: true }, + { + endpoint, + endpointBatch, + endpointMetadata, + projectPath, + dismissEndpoint, + showSuggestPopover, + }, + { + endpoint: '', + endpointBatch: '', + endpointMetadata: '', + projectPath: '', + dismissEndpoint: '', + showSuggestPopover: true, + }, [ { type: types.SET_BASE_CONFIG, - payload: { endpoint, projectPath, dismissEndpoint, showSuggestPopover }, + payload: { + endpoint, + endpointMetadata, + endpointBatch, + projectPath, + dismissEndpoint, + showSuggestPopover, + }, }, ], [], @@ -114,6 +139,64 @@ describe('DiffsStoreActions', () => { }); }); + describe('fetchDiffFilesBatch', () => { + it('should fetch batch diff files', done => { + const endpointBatch = '/fetch/diffs_batch'; + const batch1 = `${endpointBatch}?per_page=10`; + const batch2 = `${endpointBatch}?per_page=10&page=2`; + const mock = new MockAdapter(axios); + const res1 = { diff_files: [], pagination: { next_page: 2 } }; + const res2 = { diff_files: [], pagination: {} }; + mock.onGet(batch1).reply(200, res1); + mock.onGet(batch2).reply(200, res2); + + testAction( + fetchDiffFilesBatch, + {}, + { endpointBatch }, + [ + { type: types.SET_BATCH_LOADING, payload: true }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } }, + { type: types.SET_BATCH_LOADING, payload: false }, + { type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } }, + { type: types.SET_BATCH_LOADING, payload: false }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + + describe('fetchDiffFilesMeta', () => { + it('should fetch diff meta information', done => { + const endpointMetadata = '/fetch/diffs_meta'; + const mock = new MockAdapter(axios); + const data = { diff_files: [] }; + const res = { data }; + mock.onGet(endpointMetadata).reply(200, res); + + testAction( + fetchDiffFilesMeta, + {}, + { endpointMetadata }, + [ + { type: types.SET_LOADING, payload: true }, + { type: types.SET_LOADING, payload: false }, + { type: types.SET_MERGE_REQUEST_DIFFS, payload: [] }, + { type: types.SET_DIFF_DATA, payload: { data, diff_files: [] } }, + ], + [], + () => { + mock.restore(); + done(); + }, + ); + }); + }); + describe('setHighlightedRow', () => { it('should mark currently selected diff and set lineHash and fileHash of highlightedRow', () => { testAction(setHighlightedRow, 'ABC_123', {}, [ diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 3e033b6c9dc..19bf5bdd592 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -28,6 +28,16 @@ describe('DiffsStoreMutations', () => { }); }); + describe('SET_BATCH_LOADING', () => { + it('should set loading state', () => { + const state = {}; + + mutations[types.SET_BATCH_LOADING](state, false); + + expect(state.isBatchLoading).toEqual(false); + }); + }); + describe('SET_DIFF_DATA', () => { it('should set diff data type properly', () => { const state = {}; @@ -45,6 +55,23 @@ describe('DiffsStoreMutations', () => { }); }); + describe('SET_DIFFSET_DIFF_DATA_BATCH_DATA', () => { + it('should set diff data batch type properly', () => { + const state = { diffFiles: [] }; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA_BATCH](state, diffMock); + + const firstLine = state.diffFiles[0].parallel_diff_lines[0]; + + expect(firstLine.right.text).toBeUndefined(); + expect(state.diffFiles[0].renderIt).toEqual(true); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + describe('SET_DIFF_VIEW_TYPE', () => { it('should set diff view type properly', () => { const state = {}; diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index 0dcd8868aba..10d37c86ea7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -10,7 +10,6 @@ describe('Environment', () => { endpoint: 'environments.json', canCreateEnvironment: true, canReadEnvironment: true, - cssContainerClass: 'container', newEnvironmentPath: 'environments/new', helpPagePath: 'help', canaryDeploymentFeatureId: 'canary_deployment', diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js index 7f7d7b1cdbf..419f70e41af 100644 --- a/spec/javascripts/frequent_items/mock_data.js +++ b/spec/javascripts/frequent_items/mock_data.js @@ -68,7 +68,7 @@ export const mockFrequentGroups = [ }, ]; -export const mockSearchedGroups = { data: [mockRawGroup] }; +export const mockSearchedGroups = [mockRawGroup]; export const mockProcessedSearchedGroups = [mockGroup]; export const mockProject = { diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index 540fc8a21f1..6114ce8d51f 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -49,7 +49,7 @@ describe('Markdown component', () => { }); Vue.nextTick(() => { - expect(vm.$el.querySelector('a')).toBeNull(); + expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); done(); }); diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js index fecc0d604b1..2ad9428dd6f 100644 --- a/spec/javascripts/notes/components/discussion_counter_spec.js +++ b/spec/javascripts/notes/components/discussion_counter_spec.js @@ -27,6 +27,8 @@ describe('DiscussionCounter component', () => { describe('methods', () => { describe('jumpToFirstUnresolvedDiscussion', () => { it('expands unresolved discussion', () => { + window.mrTabs.currentAction = 'show'; + spyOn(vm, 'expandDiscussion').and.stub(); const discussions = [ { @@ -47,14 +49,39 @@ describe('DiscussionCounter component', () => { ...store.state, discussions, }); - setFixtures(` - <div class="discussion" data-discussion-id="${firstDiscussionId}"></div> - `); - vm.jumpToFirstUnresolvedDiscussion(); expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId }); }); + + it('jumps to first unresolved discussion from diff tab if all diff discussions are resolved', () => { + window.mrTabs.currentAction = 'diff'; + spyOn(vm, 'switchToDiscussionsTabAndJumpTo').and.stub(); + + const unresolvedId = discussionMock.id + 1; + const discussions = [ + { + ...discussionMock, + id: discussionMock.id, + diff_discussion: true, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + resolved: true, + }, + { + ...discussionMock, + id: unresolvedId, + notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }], + resolved: false, + }, + ]; + store.replaceState({ + ...store.state, + discussions, + }); + vm.jumpToFirstUnresolvedDiscussion(); + + expect(vm.switchToDiscussionsTabAndJumpTo).toHaveBeenCalledWith(unresolvedId); + }); }); }); }); diff --git a/spec/javascripts/releases/list/components/app_spec.js b/spec/javascripts/releases/list/components/app_spec.js index 471c442e497..994488581d7 100644 --- a/spec/javascripts/releases/list/components/app_spec.js +++ b/spec/javascripts/releases/list/components/app_spec.js @@ -1,15 +1,22 @@ import Vue from 'vue'; +import _ from 'underscore'; import app from '~/releases/list/components/app.vue'; import createStore from '~/releases/list/store'; import api from '~/api'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { resetStore } from '../store/helpers'; -import { releases } from '../../mock_data'; +import { + pageInfoHeadersWithoutPagination, + pageInfoHeadersWithPagination, + release, + releases, +} from '../../mock_data'; describe('Releases App ', () => { const Component = Vue.extend(app); let store; let vm; + let releasesPagination; const props = { projectId: 'gitlab-ce', @@ -19,6 +26,7 @@ describe('Releases App ', () => { beforeEach(() => { store = createStore(); + releasesPagination = _.range(21).map(index => ({ ...release, tag_name: `${index}.00` })); }); afterEach(() => { @@ -28,7 +36,7 @@ describe('Releases App ', () => { describe('while loading', () => { beforeEach(() => { - spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] })); + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} })); vm = mountComponentWithStore(Component, { props, store }); }); @@ -36,6 +44,7 @@ describe('Releases App ', () => { expect(vm.$el.querySelector('.js-loading')).not.toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); setTimeout(() => { done(); @@ -45,7 +54,9 @@ describe('Releases App ', () => { describe('with successful request', () => { beforeEach(() => { - spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases })); + spyOn(api, 'releases').and.returnValue( + Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }), + ); vm = mountComponentWithStore(Component, { props, store }); }); @@ -54,6 +65,27 @@ describe('Releases App ', () => { expect(vm.$el.querySelector('.js-loading')).toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + + done(); + }, 0); + }); + }); + + describe('with successful request and pagination', () => { + beforeEach(() => { + spyOn(api, 'releases').and.returnValue( + Promise.resolve({ data: releasesPagination, headers: pageInfoHeadersWithPagination }), + ); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders success state', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull(); done(); }, 0); @@ -62,7 +94,7 @@ describe('Releases App ', () => { describe('with empty request', () => { beforeEach(() => { - spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [] })); + spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} })); vm = mountComponentWithStore(Component, { props, store }); }); @@ -71,6 +103,7 @@ describe('Releases App ', () => { expect(vm.$el.querySelector('.js-loading')).toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull(); expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); done(); }, 0); diff --git a/spec/javascripts/releases/list/store/actions_spec.js b/spec/javascripts/releases/list/store/actions_spec.js index 8e78a631a5f..c4b49c39e28 100644 --- a/spec/javascripts/releases/list/store/actions_spec.js +++ b/spec/javascripts/releases/list/store/actions_spec.js @@ -7,14 +7,17 @@ import { import state from '~/releases/list/store/state'; import * as types from '~/releases/list/store/mutation_types'; import api from '~/api'; +import { parseIntPagination } from '~/lib/utils/common_utils'; import testAction from 'spec/helpers/vuex_action_helper'; -import { releases } from '../../mock_data'; +import { pageInfoHeadersWithoutPagination, releases } from '../../mock_data'; describe('Releases State actions', () => { let mockedState; + let pageInfo; beforeEach(() => { mockedState = state(); + pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); }); describe('requestReleases', () => { @@ -25,12 +28,16 @@ describe('Releases State actions', () => { describe('fetchReleases', () => { describe('success', () => { - it('dispatches requestReleases and receiveReleasesSuccess ', done => { - spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: releases })); + it('dispatches requestReleases and receiveReleasesSuccess', done => { + spyOn(api, 'releases').and.callFake((id, options) => { + expect(id).toEqual(1); + expect(options.page).toEqual('1'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); testAction( fetchReleases, - releases, + { projectId: 1 }, mockedState, [], [ @@ -38,7 +45,31 @@ describe('Releases State actions', () => { type: 'requestReleases', }, { - payload: releases, + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, + type: 'receiveReleasesSuccess', + }, + ], + done, + ); + }); + + it('dispatches requestReleases and receiveReleasesSuccess on page two', done => { + spyOn(api, 'releases').and.callFake((_, options) => { + expect(options.page).toEqual('2'); + return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }); + }); + + testAction( + fetchReleases, + { page: '2', projectId: 1 }, + mockedState, + [], + [ + { + type: 'requestReleases', + }, + { + payload: { data: releases, headers: pageInfoHeadersWithoutPagination }, type: 'receiveReleasesSuccess', }, ], @@ -48,12 +79,12 @@ describe('Releases State actions', () => { }); describe('error', () => { - it('dispatches requestReleases and receiveReleasesError ', done => { + it('dispatches requestReleases and receiveReleasesError', done => { spyOn(api, 'releases').and.returnValue(Promise.reject()); testAction( fetchReleases, - null, + { projectId: null }, mockedState, [], [ @@ -74,9 +105,9 @@ describe('Releases State actions', () => { it('should commit RECEIVE_RELEASES_SUCCESS mutation', done => { testAction( receiveReleasesSuccess, - releases, + { data: releases, headers: pageInfoHeadersWithoutPagination }, mockedState, - [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: releases }], + [{ type: types.RECEIVE_RELEASES_SUCCESS, payload: { pageInfo, data: releases } }], [], done, ); diff --git a/spec/javascripts/releases/list/store/mutations_spec.js b/spec/javascripts/releases/list/store/mutations_spec.js index d2577891495..d756c69d53b 100644 --- a/spec/javascripts/releases/list/store/mutations_spec.js +++ b/spec/javascripts/releases/list/store/mutations_spec.js @@ -1,13 +1,16 @@ import state from '~/releases/list/store/state'; import mutations from '~/releases/list/store/mutations'; import * as types from '~/releases/list/store/mutation_types'; -import { releases } from '../../mock_data'; +import { parseIntPagination } from '~/lib/utils/common_utils'; +import { pageInfoHeadersWithoutPagination, releases } from '../../mock_data'; describe('Releases Store Mutations', () => { let stateCopy; + let pageInfo; beforeEach(() => { stateCopy = state(); + pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); }); describe('REQUEST_RELEASES', () => { @@ -20,7 +23,7 @@ describe('Releases Store Mutations', () => { describe('RECEIVE_RELEASES_SUCCESS', () => { beforeEach(() => { - mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, releases); + mutations[types.RECEIVE_RELEASES_SUCCESS](stateCopy, { pageInfo, data: releases }); }); it('sets is loading to false', () => { @@ -34,6 +37,10 @@ describe('Releases Store Mutations', () => { it('sets data', () => { expect(stateCopy.releases).toEqual(releases); }); + + it('sets pageInfo', () => { + expect(stateCopy.pageInfo).toEqual(pageInfo); + }); }); describe('RECEIVE_RELEASES_ERROR', () => { @@ -42,6 +49,7 @@ describe('Releases Store Mutations', () => { expect(stateCopy.isLoading).toEqual(false); expect(stateCopy.releases).toEqual([]); + expect(stateCopy.pageInfo).toEqual({}); }); }); }); diff --git a/spec/javascripts/releases/mock_data.js b/spec/javascripts/releases/mock_data.js index 7197eb7bca8..72875dff172 100644 --- a/spec/javascripts/releases/mock_data.js +++ b/spec/javascripts/releases/mock_data.js @@ -1,3 +1,21 @@ +export const pageInfoHeadersWithoutPagination = { + 'X-NEXT-PAGE': '', + 'X-PAGE': '1', + 'X-PER-PAGE': '20', + 'X-PREV-PAGE': '', + 'X-TOTAL': '19', + 'X-TOTAL-PAGES': '1', +}; + +export const pageInfoHeadersWithPagination = { + 'X-NEXT-PAGE': '2', + 'X-PAGE': '1', + 'X-PER-PAGE': '20', + 'X-PREV-PAGE': '', + 'X-TOTAL': '21', + 'X-TOTAL-PAGES': '2', +}; + export const release = { name: 'Bionic Beaver', tag_name: '18.04', diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js deleted file mode 100644 index 2af4abc299a..00000000000 --- a/spec/javascripts/vue_shared/components/expand_button_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import expandButton from '~/vue_shared/components/expand_button.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('expand button', () => { - const Component = Vue.extend(expandButton); - let vm; - - beforeEach(() => { - vm = mountComponent(Component, { - slots: { - expanded: '<p>Expanded!</p>', - }, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders a collapsed button', () => { - expect(vm.$children[0].iconTestClass).toEqual('ic-ellipsis_h'); - }); - - it('hides expander on click', done => { - vm.$el.querySelector('button').click(); - vm.$nextTick(() => { - expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); - done(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js deleted file mode 100644 index da984175f9f..00000000000 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ /dev/null @@ -1,173 +0,0 @@ -import $ from 'jquery'; -import '~/behaviors/markdown/render_gfm'; -import Vue from 'vue'; -import AxiosMockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import fieldComponent from '~/vue_shared/components/markdown/field.vue'; -import { TEST_HOST } from 'spec/test_constants'; - -function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) { - expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite); - expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite); - expect(vm.$el.querySelector('.md-preview-holder').style.display).toEqual(isWrite ? 'none' : ''); -} - -describe('Markdown field component', () => { - const markdownPreviewPath = `${TEST_HOST}/preview`; - const markdownDocsPath = `${TEST_HOST}/docs`; - let axiosMock; - let vm; - - beforeEach(done => { - axiosMock = new AxiosMockAdapter(axios); - vm = new Vue({ - components: { - fieldComponent, - }, - data() { - return { - text: 'testing\n123', - }; - }, - template: ` - <field-component - markdown-preview-path="${markdownPreviewPath}" - markdown-docs-path="${markdownDocsPath}" - > - <textarea - slot="textarea" - v-model="text"> - </textarea> - </field-component> - `, - }).$mount(); - - Vue.nextTick(done); - }); - - afterEach(() => { - axiosMock.restore(); - }); - - describe('mounted', () => { - const previewHTML = '<p>markdown preview</p>'; - - it('renders textarea inside backdrop', () => { - expect(vm.$el.querySelector('.zen-backdrop textarea')).not.toBeNull(); - }); - - describe('markdown preview', () => { - let previewLink; - let writeLink; - - beforeEach(() => { - axiosMock.onPost(markdownPreviewPath).replyOnce(200, { body: previewHTML }); - - previewLink = vm.$el.querySelector('.nav-links .js-preview-link'); - writeLink = vm.$el.querySelector('.nav-links .js-write-link'); - }); - - it('sets preview link as active', done => { - previewLink.click(); - - Vue.nextTick(() => { - expect(previewLink.parentNode.classList.contains('active')).toBeTruthy(); - - done(); - }); - }); - - it('shows preview loading text', done => { - previewLink.click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.md-preview-holder').textContent.trim()).toContain( - 'Loading…', - ); - - done(); - }); - }); - - it('renders markdown preview', done => { - previewLink.click(); - - setTimeout(() => { - expect(vm.$el.querySelector('.md-preview-holder').innerHTML).toContain(previewHTML); - - done(); - }); - }); - - it('renders GFM with jQuery', done => { - spyOn($.fn, 'renderGFM'); - - previewLink.click(); - - setTimeout(() => { - expect($.fn.renderGFM).toHaveBeenCalled(); - - done(); - }, 0); - }); - - it('clicking already active write or preview link does nothing', done => { - writeLink.click(); - Vue.nextTick() - .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm)) - .then(() => writeLink.click()) - .then(() => Vue.nextTick()) - .then(() => assertMarkdownTabs(true, writeLink, previewLink, vm)) - .then(() => previewLink.click()) - .then(() => Vue.nextTick()) - .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm)) - .then(() => previewLink.click()) - .then(() => Vue.nextTick()) - .then(() => assertMarkdownTabs(false, writeLink, previewLink, vm)) - .then(done) - .catch(done.fail); - }); - }); - - describe('markdown buttons', () => { - it('converts single words', done => { - const textarea = vm.$el.querySelector('textarea'); - - textarea.setSelectionRange(0, 7); - vm.$el.querySelector('.js-md').click(); - - Vue.nextTick(() => { - expect(textarea.value).toContain('**testing**'); - - done(); - }); - }); - - it('converts a line', done => { - const textarea = vm.$el.querySelector('textarea'); - - textarea.setSelectionRange(0, 0); - vm.$el.querySelectorAll('.js-md')[5].click(); - - Vue.nextTick(() => { - expect(textarea.value).toContain('* testing'); - - done(); - }); - }); - - it('converts multiple lines', done => { - const textarea = vm.$el.querySelector('textarea'); - - textarea.setSelectionRange(0, 50); - vm.$el.querySelectorAll('.js-md')[5].click(); - - Vue.nextTick(() => { - expect(textarea.value).toContain('* testing\n* 123'); - - done(); - }); - }); - }); - }); -}); diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb index 3adde213f59..1b73b9a083d 100644 --- a/spec/lib/gitlab/ci/build/context/build_spec.rb +++ b/spec/lib/gitlab/ci/build/context/build_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Ci::Build::Context::Build do diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb index 6bc8f862779..65cc41ed3f9 100644 --- a/spec/lib/gitlab/ci/build/context/global_spec.rb +++ b/spec/lib/gitlab/ci/build/context/global_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Ci::Build::Context::Global do diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb index dad4f408e50..ffd24aa56b9 100644 --- a/spec/lib/gitlab/ci/config/entry/default_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb @@ -26,7 +26,8 @@ describe Gitlab::Ci::Config::Entry::Default do it 'contains the expected node names' do expect(described_class.nodes.keys) .to match_array(%i[before_script image services - after_script cache interruptible]) + after_script cache interruptible + timeout retry]) end end end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 7b72b45fd8d..c80b54bd6be 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -241,4 +241,28 @@ describe Gitlab::Ci::Config::Entry::Environment do end end end + + describe 'kubernetes' do + let(:config) do + { name: 'production', kubernetes: kubernetes_config } + end + + context 'is a string' do + let(:kubernetes_config) { 'production' } + + it { expect(entry).not_to be_valid } + end + + context 'is a hash' do + let(:kubernetes_config) { Hash(namespace: 'production') } + + it { expect(entry).to be_valid } + end + + context 'is nil' do + let(:kubernetes_config) { nil } + + it { expect(entry).to be_valid } + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index fe83171c57a..b0e08e49d78 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Ci::Config::Entry::Job do let(:result) do %i[before_script script stage type after_script cache image services only except rules needs variables artifacts - environment coverage retry interruptible] + environment coverage retry interruptible timeout] end it { is_expected.to match_array result } @@ -417,21 +417,21 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when timeout value is not correct' do context 'when it is higher than instance wide timeout' do - let(:config) { { timeout: '3 months' } } + let(:config) { { timeout: '3 months', script: 'test' } } it 'returns error about value too high' do expect(entry).not_to be_valid expect(entry.errors) - .to include "job timeout should not exceed the limit" + .to include "timeout config should not exceed the limit" end end context 'when it is not a duration' do - let(:config) { { timeout: 100 } } + let(:config) { { timeout: 100, script: 'test' } } it 'returns error about wrong value' do expect(entry).not_to be_valid - expect(entry.errors).to include 'job timeout should be a duration' + expect(entry.errors).to include 'timeout config should be a duration' end end end diff --git a/spec/lib/gitlab/ci/config/entry/kubernetes_spec.rb b/spec/lib/gitlab/ci/config/entry/kubernetes_spec.rb new file mode 100644 index 00000000000..468e83ec506 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/kubernetes_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Kubernetes do + subject { described_class.new(config) } + + describe 'attributes' do + it { is_expected.to respond_to(:namespace) } + it { is_expected.to respond_to(:has_namespace?) } + end + + describe 'validations' do + describe 'config' do + context 'is a hash containing known keys' do + let(:config) { Hash(namespace: 'namespace') } + + it { is_expected.to be_valid } + end + + context 'is a hash containing an unknown key' do + let(:config) { Hash(unknown: 'attribute') } + + it { is_expected.not_to be_valid } + end + + context 'is a string' do + let(:config) { 'config' } + + it { is_expected.not_to be_valid } + end + end + + describe 'namespace' do + let(:config) { Hash(namespace: namespace) } + + context 'is a string' do + let(:namespace) { 'namespace' } + + it { is_expected.to be_valid } + end + + context 'is a hash' do + let(:namespace) { Hash(key: 'namespace') } + + it { is_expected.not_to be_valid } + end + + context 'is not present' do + let(:namespace) { '' } + + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index af4e9d687c4..385df72fa41 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -152,6 +152,22 @@ describe Gitlab::Ci::CronParser do end end end + + context 'when time crosses a Daylight Savings boundary' do + let(:cron) { '* 0 1 12 *'} + + # Note this previously only failed if the time zone is set + # to a zone that observes Daylight Savings + # (e.g. America/Chicago) at the start of the test. Stubbing + # TZ doesn't appear to be enough. + it 'generates day without TZInfo::AmbiguousTime error' do + Timecop.freeze(Time.utc(2020, 1, 1)) do + expect(subject.year).to eq(2020) + expect(subject.month).to eq(12) + expect(subject.day).to eq(1) + end + end + end end end end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..c2f9930056a --- /dev/null +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Auto-DevOps.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') } + + describe 'the created pipeline' do + let(:user) { create(:admin) } + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + it 'creates a build and a test job' do + expect(build_names).to include('build', 'test') + end + + context 'when the project has no active cluster' do + it 'only creates a build and a test stage' do + expect(pipeline.stages_names).to eq(%w(build test)) + end + + it 'does not create any deployment-related builds' do + expect(build_names).not_to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end + + context 'when the project has an active cluster' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + + before do + allow(cluster).to receive(:active?).and_return(true) + end + + describe 'deployment-related builds' do + context 'on default branch' do + it 'does not include rollout jobs besides production' do + expect(build_names).to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + + context 'when STAGING_ENABLED=1' do + before do + create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: '1') + end + + it 'includes a staging job and a production_manual job' do + expect(build_names).not_to include('production') + expect(build_names).to include('production_manual') + expect(build_names).to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end + + context 'when CANARY_ENABLED=1' do + before do + create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: '1') + end + + it 'includes a canary job and a production_manual job' do + expect(build_names).not_to include('production') + expect(build_names).to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end + end + + context 'outside of default branch' do + let(:pipeline_branch) { 'patch-1' } + + before do + project.repository.create_branch(pipeline_branch) + end + + it 'does not include rollout jobs besides review' do + expect(build_names).not_to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 4b1c7483b11..5dc51f83b3c 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -149,6 +149,28 @@ module Gitlab expect(subject[:options]).not_to have_key(:retry) end end + + context 'when retry count is specified by default' do + let(:config) do + YAML.dump(default: { retry: { max: 1 } }, + rspec: { script: 'rspec' }) + end + + it 'does use the default value' do + expect(subject[:options]).to include(retry: { max: 1 }) + end + end + + context 'when retry count default value is overridden' do + let(:config) do + YAML.dump(default: { retry: { max: 1 } }, + rspec: { script: 'rspec', retry: { max: 2 } }) + end + + it 'does use the job value' do + expect(subject[:options]).to include(retry: { max: 2 }) + end + end end describe 'allow failure entry' do @@ -1375,7 +1397,7 @@ module Gitlab end it 'raises an error for invalid number' do - expect { builds }.to raise_error('jobs:deploy_to_production timeout should be a duration') + expect { builds }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:deploy_to_production:timeout config should be a duration') end end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index 35edfa08a63..bf6152ff3c2 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -33,8 +33,8 @@ describe Gitlab::Danger::Teammate do context 'when labels contain devops::create and the category is test' do let(:labels) { ['devops::create'] } - context 'when role is Test Automation Engineer, Create' do - let(:role) { 'Test Automation Engineer, Create' } + context 'when role is Software Engineer in Test, Create' do + let(:role) { 'Software Engineer in Test, Create' } it '#reviewer? returns true' do expect(subject.reviewer?(project, :test, labels)).to be_truthy @@ -45,7 +45,7 @@ describe Gitlab::Danger::Teammate do end context 'when hyperlink is mangled in the role' do - let(:role) { '<a href="#">Test Automation Engineer</a>, Create' } + let(:role) { '<a href="#">Software Engineer in Test</a>, Create' } it '#reviewer? returns true' do expect(subject.reviewer?(project, :test, labels)).to be_truthy @@ -53,16 +53,16 @@ describe Gitlab::Danger::Teammate do end end - context 'when role is Test Automation Engineer' do - let(:role) { 'Test Automation Engineer' } + context 'when role is Software Engineer in Test' do + let(:role) { 'Software Engineer in Test' } it '#reviewer? returns false' do expect(subject.reviewer?(project, :test, labels)).to be_falsey end end - context 'when role is Test Automation Engineer, Manage' do - let(:role) { 'Test Automation Engineer, Manage' } + context 'when role is Software Engineer in Test, Manage' do + let(:role) { 'Software Engineer in Test, Manage' } it '#reviewer? returns false' do expect(subject.reviewer?(project, :test, labels)).to be_falsey diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index 5b1a17e734d..ee3c99afdf1 100644 --- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -279,5 +279,11 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do end end end + + it "tracks successful install" do + expect(Gitlab::Tracking).to receive(:event).with("self_monitoring", "project_created") + + result + end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 52d6a86f7d0..cd593390821 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -177,6 +177,25 @@ describe Gitlab::Gpg do end.not_to raise_error end + it 'tracks an exception when cleaning up the tmp dir fails' do + expected_exception = described_class::CleanupError.new('cleanup failed') + expected_tmp_dir = nil + + expect(described_class).to receive(:cleanup_tmp_dir).and_raise(expected_exception) + allow(Gitlab::Sentry).to receive(:track_exception) + + described_class.using_tmp_keychain do + expected_tmp_dir = described_class.current_home_dir + FileUtils.touch(File.join(expected_tmp_dir, 'dummy.file')) + end + + expect(Gitlab::Sentry).to have_received(:track_exception).with( + expected_exception, + issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918', + extra: { tmp_dir: expected_tmp_dir, contents: ['dummy.file'] } + ) + end + shared_examples 'multiple deletion attempts of the tmp-dir' do |seconds| let(:tmp_dir) do tmp_dir = Dir.mktmpdir @@ -211,15 +230,6 @@ describe Gitlab::Gpg do expect(File.exist?(tmp_dir)).to be false end - - it 'does not retry when the feature flag is disabled' do - stub_feature_flags(gpg_cleanup_retries: false) - - expect(FileUtils).to receive(:remove_entry).with(tmp_dir, true).and_call_original - expect(Retriable).not_to receive(:retriable) - - described_class.using_tmp_keychain {} - end end it_behaves_like 'multiple deletion attempts of the tmp-dir', described_class::FG_CLEANUP_RUNTIME_S diff --git a/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb index e21af023bb8..0cfda80b854 100644 --- a/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb index f47b9dd3498..c0762e9892b 100644 --- a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb +++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb index 23762666ba8..8f02b979598 100644 --- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb +++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Graphql::Authorize::AuthorizeResource do diff --git a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb index 9dda2a41ec6..36955019863 100644 --- a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb @@ -218,23 +218,11 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do end end - # TODO Enable this as part of below issue - # https://gitlab.com/gitlab-org/gitlab/issues/32933 - # context 'when an invalid cursor is provided' do - # let(:arguments) { { before: 'invalidcursor' } } - # - # it 'raises an error' do - # expect { expect(subject.sliced_nodes) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - # end - # end - - # TODO Remove this as part of below issue - # https://gitlab.com/gitlab-org/gitlab/issues/32933 - context 'when an old style cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64(projects[1].id.to_s, padding: false) } } + context 'when an invalid cursor is provided' do + let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) + it 'raises an error' do + expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) end end end diff --git a/spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb deleted file mode 100644 index aaf28fed684..00000000000 --- a/spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true - -# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104 -require 'spec_helper' - -describe Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection do - describe 'old keyset_connection' do - let(:described_class) { Gitlab::Graphql::Connections::Keyset::Connection } - let(:nodes) { Project.all.order(id: :asc) } - let(:arguments) { {} } - subject(:connection) do - described_class.new(nodes, arguments, max_page_size: 3) - end - - before do - stub_feature_flags(graphql_keyset_pagination: false) - end - - def encoded_property(value) - Base64Bp.urlsafe_encode64(value.to_s, padding: false) - end - - describe '#cursor_from_nodes' do - let(:project) { create(:project) } - - it 'returns an encoded ID' do - expect(connection.cursor_from_node(project)) - .to eq(encoded_property(project.id)) - end - - context 'when an order was specified' do - let(:nodes) { Project.order(:updated_at) } - - it 'returns the encoded value of the order' do - expect(connection.cursor_from_node(project)) - .to eq(encoded_property(project.updated_at)) - end - end - end - - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } - - context 'when before is passed' do - let(:arguments) { { before: encoded_property(projects[1].id) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1]) - end - end - end - - context 'when after is passed' do - let(:arguments) { { after: encoded_property(projects[1].id) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1]) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - end - end - - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_property(projects[1].id), - before: encoded_property(projects[3].id) - } - end - - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) - end - end - end - - describe '#paged_nodes' do - let!(:projects) { create_list(:project, 5) } - - it 'returns the collection limited to max page size' do - expect(subject.paged_nodes.size).to eq(3) - end - - it 'is a loaded memoized array' do - expect(subject.paged_nodes).to be_an(Array) - expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id) - end - - context 'when `first` is passed' do - let(:arguments) { { first: 2 } } - - it 'returns only the first elements' do - expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second) - end - end - - context 'when `last` is passed' do - let(:arguments) { { last: 2 } } - - it 'returns only the last elements' do - expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4]) - end - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb index 22d8aa4274a..1e8de144b8d 100644 --- a/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb +++ b/spec/lib/gitlab/graphql/loaders/batch_lfs_oid_loader_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Graphql::Loaders::BatchLfsOidLoader do diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb index a4bbd868558..79f9ecb39cf 100644 --- a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb +++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Graphql::Loaders::BatchModelLoader do diff --git a/spec/lib/gitlab/graphs/commits_spec.rb b/spec/lib/gitlab/graphs/commits_spec.rb index 09654e0439e..f92c7fb11a1 100644 --- a/spec/lib/gitlab/graphs/commits_spec.rb +++ b/spec/lib/gitlab/graphs/commits_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Graphs::Commits do diff --git a/spec/lib/gitlab/health_checks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb index 33c6c24449c..3c1c1e3818d 100644 --- a/spec/lib/gitlab/health_checks/db_check_spec.rb +++ b/spec/lib/gitlab/health_checks/db_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative './simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb index 36e2fd04aeb..d4ce16ce6fc 100644 --- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb +++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HealthChecks::GitalyCheck do diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb index 91441a7ddc3..cb20c1188af 100644 --- a/spec/lib/gitlab/health_checks/master_check_spec.rb +++ b/spec/lib/gitlab/health_checks/master_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative './simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/puma_check_spec.rb b/spec/lib/gitlab/health_checks/puma_check_spec.rb index 71b6386b174..dd052a4dd2c 100644 --- a/spec/lib/gitlab/health_checks/puma_check_spec.rb +++ b/spec/lib/gitlab/health_checks/puma_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HealthChecks::PumaCheck do diff --git a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb b/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb index 3693f52b51b..aaf474d7eeb 100644 --- a/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis/cache_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative '../simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb b/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb index c69443d205d..f4b5e18da2a 100644 --- a/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis/queues_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative '../simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb index 03afc1cd761..ae7ee0d0859 100644 --- a/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis/redis_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative '../simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb b/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb index b72e152bbe2..3e92b072254 100644 --- a/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb +++ b/spec/lib/gitlab/health_checks/redis/shared_state_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require_relative '../simple_check_shared' diff --git a/spec/lib/gitlab/health_checks/simple_check_shared.rb b/spec/lib/gitlab/health_checks/simple_check_shared.rb index 03a7cf249cf..3d0f9b3cf7a 100644 --- a/spec/lib/gitlab/health_checks/simple_check_shared.rb +++ b/spec/lib/gitlab/health_checks/simple_check_shared.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + shared_context 'simple_check' do |metrics_prefix, check_name, success_result| describe '#metrics' do subject { described_class.metrics } diff --git a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb index c02d0c37738..931b61cb168 100644 --- a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb +++ b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HealthChecks::UnicornCheck do diff --git a/spec/lib/gitlab/hook_data/base_builder_spec.rb b/spec/lib/gitlab/hook_data/base_builder_spec.rb index e3c5ee3b905..ce8610a2108 100644 --- a/spec/lib/gitlab/hook_data/base_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/base_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HookData::BaseBuilder do diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb index 97a89b319ea..5135c84df19 100644 --- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HookData::IssuableBuilder do diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb index ebd7feb0055..8008f3d72b2 100644 --- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HookData::IssueBuilder do diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb index 39f80f92fa6..506354e370c 100644 --- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::HookData::MergeRequestBuilder do diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb index a399517cc04..2d8bb538681 100644 --- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb +++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::I18n::MetadataEntry do diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index 3dbc23d2aaf..2ab363ee45c 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'simple_po_parser' diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index b301e6ea443..3f0b922cc51 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::I18n::TranslationEntry do diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb index a3d2880182d..86ceb97b250 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb index 21a227335cd..95c47d15f8f 100644 --- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do diff --git a/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb index bf727285a9f..9fe9e2eb73d 100644 --- a/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb +++ b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AfterExportStrategyBuilder do diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb index 873728f9909..44192c4639d 100644 --- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AttributeCleaner do @@ -24,7 +26,10 @@ describe Gitlab::ImportExport::AttributeCleaner do '_html' => '<p>perfectly ordinary html</p>', 'cached_markdown_version' => 12345, 'group_id' => 99, - 'commit_id' => 99 + 'commit_id' => 99, + 'issue_ids' => [1, 2, 3], + 'merge_request_ids' => [1, 2, 3], + 'note_ids' => [1, 2, 3] } end diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb index cc8ca1d87e3..41da1383f74 100644 --- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' # Part of the test security suite for the Import/Export feature diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index e44ff6bbcbd..77f551eeca1 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer do diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index 2bd1b9924c6..a84406f9784 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::AvatarSaver do diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index b190a1007a0..6f90798f815 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::FastHashSerializer do diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index fbc9bcd2df5..3c5f916a879 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::FileImporter do diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index 5752fd8fa0d..10197330527 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'forked project import' do diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb index 1a5cb7806a3..0d0a2df4423 100644 --- a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::GroupProjectObjectBuilder do diff --git a/spec/lib/gitlab/import_export/hash_util_spec.rb b/spec/lib/gitlab/import_export/hash_util_spec.rb index 366582dece3..ddd874ddecf 100644 --- a/spec/lib/gitlab/import_export/hash_util_spec.rb +++ b/spec/lib/gitlab/import_export/hash_util_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::HashUtil do diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb index a6b0dc758cd..2ece0dd4b56 100644 --- a/spec/lib/gitlab/import_export/import_export_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport do diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 898e4d07760..942af4084e5 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::Importer do diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb index 2b0bdb909ae..75d6da48f33 100644 --- a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::LfsRestorer do diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb index c3c88486e16..89493c3bf27 100644 --- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb +++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::LfsSaver do diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index a9e8431acba..9847166a4d1 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::MembersMapper do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 4b234411a44..e7f039d7a6f 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb index 4426e68b474..d97e76f3cbd 100644 --- a/spec/lib/gitlab/import_export/model_configuration_spec.rb +++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' # Part of the test security suite for the Import/Export feature diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 64a648ca1f8..2d8a603172d 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' include ImportExport::CommonUtil diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 97d8b155826..29d0099d5c1 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeSaver do diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 87f665bd995..802c00bed74 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::Reader do diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index a23e68a8f00..8669b9ce7eb 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::RelationFactory do diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index e2ffb2adb9b..4f8075d9704 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::RepoRestorer do diff --git a/spec/lib/gitlab/import_export/repo_saver_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb index c3df371af43..54dbefdb535 100644 --- a/spec/lib/gitlab/import_export/repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::RepoSaver do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index a16c23832e2..704d0184cf1 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -651,6 +651,7 @@ PrometheusAlert: - prometheus_metric_id Badge: - id +- name - link_url - image_url - project_id @@ -727,6 +728,7 @@ List: - milestone_id - user_id - max_issue_count +- max_issue_weight ExternalPullRequest: - id - created_at diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb index aca63953677..279d99dc820 100644 --- a/spec/lib/gitlab/import_export/saver_spec.rb +++ b/spec/lib/gitlab/import_export/saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'fileutils' diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index fc011f7e1be..2b94610a074 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'fileutils' diff --git a/spec/lib/gitlab/import_export/uploads_manager_spec.rb b/spec/lib/gitlab/import_export/uploads_manager_spec.rb index f13f639d6b7..04a94954187 100644 --- a/spec/lib/gitlab/import_export/uploads_manager_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_manager_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::UploadsManager do diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb index e2e8204b2fa..5c456d6f3b1 100644 --- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::UploadsRestorer do diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 24993460e51..98b00cceada 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::UploadsSaver do diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index 76f8253ec9b..6a626d864a9 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' include ImportExport::CommonUtil diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb index 249afbd23d1..9c6bbff48b7 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::WikiRepoSaver do diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb index f99f198da33..33cd3e55393 100644 --- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ImportExport::WikiRestorer do diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb index 911d6024804..64f97379926 100644 --- a/spec/lib/gitlab/kubernetes/config_map_spec.rb +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::ConfigMap do diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 0de809833e6..5d9beec093a 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Helm::Api do diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index 78a4eb44e38..c59078449b8 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Helm::BaseCommand do diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index 4a3b9d4bf6a..f87ceb45766 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Helm::InitCommand do diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index e5a361bdab3..a25b6b9a398 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index e1b4bd0b664..24a734a2915 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Helm::Pod do diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb index e91a755aa03..6dbb34c2930 100644 --- a/spec/lib/gitlab/kubernetes/namespace_spec.rb +++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Kubernetes::Namespace do diff --git a/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb index 48655851140..e96745f5fbe 100644 --- a/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/branch_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::BranchFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/client_spec.rb b/spec/lib/gitlab/legacy_github_import/client_spec.rb index 80b767abce0..194518a1f36 100644 --- a/spec/lib/gitlab/legacy_github_import/client_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::Client do diff --git a/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb index 413654e108c..0f03db312ce 100644 --- a/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/comment_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::CommentFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb index 9163019514b..b54f30947aa 100644 --- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::Importer do diff --git a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb index 3b5d8945344..f5bfc379e89 100644 --- a/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/issuable_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::IssuableFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb index 1a4d5dbfb70..9a7a34afbe7 100644 --- a/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/issue_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::IssueFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb index 0d1d04f1bf6..e56e2772f6a 100644 --- a/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/label_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::LabelFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb index 1db4bbb568c..9fa72b3cd90 100644 --- a/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/milestone_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::MilestoneFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 8675d8691c8..b0687474c80 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::ProjectCreator do diff --git a/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb index 267a41e3f32..622210508b9 100644 --- a/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/pull_request_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::PullRequestFormatter do diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index c81c69e95c7..2b90035d148 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Middleware::Go do @@ -25,7 +27,7 @@ describe Gitlab::Middleware::Go do describe 'when go-get=1' do before do env['QUERY_STRING'] = 'go-get=1' - env['PATH_INFO'] = "/#{path}" + env['PATH_INFO'] = +"/#{path}" end shared_examples 'go-get=1' do |enabled_protocol:| diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 1397add9f5a..c580b46cf8d 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -61,6 +61,12 @@ describe Gitlab::Regex do it { is_expected.to match('my/image') } it { is_expected.to match('my/awesome/image-1') } it { is_expected.to match('my/awesome/image.test') } + it { is_expected.to match('my/awesome/image--test') } + # docker distribution allows for infinite `-` + # https://github.com/docker/distribution/blob/master/reference/regexp.go#L13 + # but we have a range of 0,10 to add a reasonable limit. + it { is_expected.not_to match('my/image-----------test') } + it { is_expected.not_to match('my/image-.test') } it { is_expected.not_to match('.my/image') } it { is_expected.not_to match('my/image.') } end diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb index 0d8cff3a295..36c6f377bde 100644 --- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb @@ -3,106 +3,201 @@ require 'fast_spec_helper' describe Gitlab::SidekiqMiddleware::Metrics do - let(:middleware) { described_class.new } - let(:concurrency_metric) { double('concurrency metric') } - - let(:queue_duration_seconds) { double('queue duration seconds metric') } - let(:completion_seconds_metric) { double('completion seconds metric') } - let(:user_execution_seconds_metric) { double('user execution seconds metric') } - let(:failed_total_metric) { double('failed total metric') } - let(:retried_total_metric) { double('retried total metric') } - let(:running_jobs_metric) { double('running jobs metric') } - - before do - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) - allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) - allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) - - allow(concurrency_metric).to receive(:set) - end + context "with worker attribution" do + subject { described_class.new } - describe '#initialize' do - it 'sets general metrics' do - expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) + let(:queue) { :test } + let(:worker_class) { worker.class } + let(:job) { {} } + let(:job_status) { :done } + let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) } + let(:default_labels) { { queue: queue.to_s, boundary: "", external_dependencies: "no", feature_category: "", latency_sensitive: "no" } } + + shared_examples "a metrics middleware" do + context "with mocked prometheus" do + let(:concurrency_metric) { double('concurrency metric') } + + let(:queue_duration_seconds) { double('queue duration seconds metric') } + let(:completion_seconds_metric) { double('completion seconds metric') } + let(:user_execution_seconds_metric) { double('user execution seconds metric') } + let(:failed_total_metric) { double('failed total metric') } + let(:retried_total_metric) { double('retried total metric') } + let(:running_jobs_metric) { double('running jobs metric') } + + before do + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) + allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) + allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) + + allow(concurrency_metric).to receive(:set) + end + + describe '#initialize' do + it 'sets concurrency metrics' do + expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i) + + subject + end + end + + describe '#call' do + let(:thread_cputime_before) { 1 } + let(:thread_cputime_after) { 2 } + let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before } + + let(:monotonic_time_before) { 11 } + let(:monotonic_time_after) { 20 } + let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } + + let(:queue_duration_for_job) { 0.01 } + + before do + allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after) + allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) + allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job) + + expect(running_jobs_metric).to receive(:increment).with(labels, 1) + expect(running_jobs_metric).to receive(:increment).with(labels, -1) + + expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job + expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) + expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) + end + + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once + end + + it 'sets queue specific metrics' do + subject.call(worker, job, :test) { nil } + end + + context 'when job_duration is not available' do + let(:queue_duration_for_job) { nil } + + it 'does not set the queue_duration_seconds histogram' do + expect(queue_duration_seconds).not_to receive(:observe) + + subject.call(worker, job, :test) { nil } + end + end + + context 'when error is raised' do + let(:job_status) { :fail } + + it 'sets sidekiq_jobs_failed_total and reraises' do + expect(failed_total_metric).to receive(:increment).with(labels, 1) + + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + end + end + + context 'when job is retried' do + let(:job) { { 'retry_count' => 1 } } + + it 'sets sidekiq_jobs_retried_total metric' do + expect(retried_total_metric).to receive(:increment) + + subject.call(worker, job, :test) { nil } + end + end + end + end - middleware - end - end + context "with prometheus integrated" do + describe '#call' do + it 'yields block' do + expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once + end - it 'ignore user execution when measured 0' do - allow(completion_seconds_metric).to receive(:observe) + context 'when error is raised' do + let(:job_status) { :fail } - expect(user_execution_seconds_metric).not_to receive(:observe) - end + it 'sets sidekiq_jobs_failed_total and reraises' do + expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + end + end + end + end + end - describe '#call' do - let(:worker) { double(:worker) } + context "when workers are not attributed" do + class TestNonAttributedWorker + include Sidekiq::Worker + end + let(:worker) { TestNonAttributedWorker.new } + let(:labels) { default_labels } - let(:job) { {} } - let(:job_status) { :done } - let(:labels) { { queue: :test } } - let(:labels_with_job_status) { { queue: :test, job_status: job_status } } + it_behaves_like "a metrics middleware" + end - let(:thread_cputime_before) { 1 } - let(:thread_cputime_after) { 2 } - let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before } + context "when workers are attributed" do + def create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, category) + Class.new do + include Sidekiq::Worker + include WorkerAttributes + + latency_sensitive_worker! if latency_sensitive + worker_has_external_dependencies! if external_dependencies + worker_resource_boundary resource_boundary unless resource_boundary == :unknown + feature_category category unless category.nil? + end + end - let(:monotonic_time_before) { 11 } - let(:monotonic_time_after) { 20 } - let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } + let(:latency_sensitive) { false } + let(:external_dependencies) { false } + let(:resource_boundary) { :unknown } + let(:feature_category) { nil } + let(:worker_class) { create_attributed_worker_class(latency_sensitive, external_dependencies, resource_boundary, feature_category) } + let(:worker) { worker_class.new } - let(:queue_duration_for_job) { 0.01 } + context "latency sensitive" do + let(:latency_sensitive) { true } + let(:labels) { default_labels.merge(latency_sensitive: "yes") } - before do - allow(middleware).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after) - allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) - allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job) + it_behaves_like "a metrics middleware" + end - expect(running_jobs_metric).to receive(:increment).with(labels, 1) - expect(running_jobs_metric).to receive(:increment).with(labels, -1) + context "external dependencies" do + let(:external_dependencies) { true } + let(:labels) { default_labels.merge(external_dependencies: "yes") } - expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job - expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration) - expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration) - end + it_behaves_like "a metrics middleware" + end - it 'yields block' do - expect { |b| middleware.call(worker, job, :test, &b) }.to yield_control.once - end + context "cpu boundary" do + let(:resource_boundary) { :cpu } + let(:labels) { default_labels.merge(boundary: "cpu") } - it 'sets queue specific metrics' do - middleware.call(worker, job, :test) { nil } - end + it_behaves_like "a metrics middleware" + end - context 'when job_duration is not available' do - let(:queue_duration_for_job) { nil } + context "memory boundary" do + let(:resource_boundary) { :memory } + let(:labels) { default_labels.merge(boundary: "memory") } - it 'does not set the queue_duration_seconds histogram' do - middleware.call(worker, job, :test) { nil } + it_behaves_like "a metrics middleware" end - end - context 'when job is retried' do - let(:job) { { 'retry_count' => 1 } } + context "feature category" do + let(:feature_category) { :authentication } + let(:labels) { default_labels.merge(feature_category: "authentication") } - it 'sets sidekiq_jobs_retried_total metric' do - expect(retried_total_metric).to receive(:increment) - - middleware.call(worker, job, :test) { nil } + it_behaves_like "a metrics middleware" end - end - - context 'when error is raised' do - let(:job_status) { :fail } - it 'sets sidekiq_jobs_failed_total and reraises' do - expect(failed_total_metric).to receive(:increment).with(labels, 1) + context "combined" do + let(:latency_sensitive) { true } + let(:external_dependencies) { true } + let(:resource_boundary) { :cpu } + let(:feature_category) { :authentication } + let(:labels) { default_labels.merge(latency_sensitive: "yes", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") } - expect { middleware.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed") + it_behaves_like "a metrics middleware" end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 800ec3219e5..d34919d17fe 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -75,71 +75,6 @@ describe Ci::Pipeline, :mailer do end end - describe '.sort_by_merge_request_pipelines' do - subject { described_class.sort_by_merge_request_pipelines } - - context 'when branch pipelines exist' do - let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) } - let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) } - - it 'returns pipelines order by id' do - expect(subject).to eq([branch_pipeline_2, - branch_pipeline_1]) - end - end - - context 'when merge request pipelines exist' do - let!(:merge_request_pipeline_1) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) - end - - let!(:merge_request_pipeline_2) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) - end - - let(:merge_request) do - create(:merge_request, - source_project: project, - source_branch: 'feature', - target_project: project, - target_branch: 'master') - end - - it 'returns pipelines order by id' do - expect(subject).to eq([merge_request_pipeline_2, - merge_request_pipeline_1]) - end - end - - context 'when both branch pipeline and merge request pipeline exist' do - let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) } - let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) } - - let!(:merge_request_pipeline_1) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) - end - - let!(:merge_request_pipeline_2) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) - end - - let(:merge_request) do - create(:merge_request, - source_project: project, - source_branch: 'feature', - target_project: project, - target_branch: 'master') - end - - it 'returns merge request pipeline first' do - expect(subject).to eq([merge_request_pipeline_2, - merge_request_pipeline_1, - branch_pipeline_2, - branch_pipeline_1]) - end - end - end - describe '.for_sha' do subject { described_class.for_sha(sha) } @@ -226,39 +161,6 @@ describe Ci::Pipeline, :mailer do end end - describe '.detached_merge_request_pipelines' do - subject { described_class.detached_merge_request_pipelines(merge_request, sha) } - - let!(:pipeline) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha) - end - - let(:merge_request) { create(:merge_request) } - let(:sha) { merge_request.diff_head_sha } - - it 'returns detached merge request pipelines' do - is_expected.to eq([pipeline]) - end - - context 'when sha does not exist' do - let(:sha) { 'abc' } - - it 'returns empty array' do - is_expected.to be_empty - end - end - - context 'when pipeline is merge request pipeline' do - let!(:pipeline) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha) - end - - it 'returns empty array' do - is_expected.to be_empty - end - end - end - describe '#detached_merge_request_pipeline?' do subject { pipeline.detached_merge_request_pipeline? } @@ -278,39 +180,6 @@ describe Ci::Pipeline, :mailer do end end - describe '.merge_request_pipelines' do - subject { described_class.merge_request_pipelines(merge_request, source_sha) } - - let!(:pipeline) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha) - end - - let(:merge_request) { create(:merge_request) } - let(:source_sha) { merge_request.diff_head_sha } - - it 'returns merge pipelines' do - is_expected.to eq([pipeline]) - end - - context 'when source sha is empty' do - let(:source_sha) { nil } - - it 'returns empty array' do - is_expected.to be_empty - end - end - - context 'when pipeline is detached merge request pipeline' do - let!(:pipeline) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha) - end - - it 'returns empty array' do - is_expected.to be_empty - end - end - end - describe '#merge_request_pipeline?' do subject { pipeline.merge_request_pipeline? } @@ -330,25 +199,6 @@ describe Ci::Pipeline, :mailer do end end - describe '#merge_train_pipeline?' do - subject { pipeline.merge_train_pipeline? } - - let!(:pipeline) do - create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: ref, target_sha: 'xxx') - end - - let(:merge_request) { create(:merge_request) } - let(:ref) { 'refs/merge-requests/1/train' } - - it { is_expected.to be_truthy } - - context 'when ref is merge ref' do - let(:ref) { 'refs/merge-requests/1/merge' } - - it { is_expected.to be_falsy } - end - end - describe '#merge_request_ref?' do subject { pipeline.merge_request_ref? } @@ -359,43 +209,19 @@ describe Ci::Pipeline, :mailer do end end - describe '#merge_train_ref?' do - subject { pipeline.merge_train_ref? } - - it 'calls Mergetrain#merge_train_ref?' do - expect(MergeRequest).to receive(:merge_train_ref?).with(pipeline.ref) - - subject - end - end - describe '#merge_request_event_type' do subject { pipeline.merge_request_event_type } - before do - allow(pipeline).to receive(:merge_request_event?) { true } - end - - context 'when pipeline is merge train pipeline' do - before do - allow(pipeline).to receive(:merge_train_pipeline?) { true } - end - - it { is_expected.to eq(:merge_train) } - end + let(:pipeline) { merge_request.all_pipelines.last } context 'when pipeline is merge request pipeline' do - before do - allow(pipeline).to receive(:merge_request_pipeline?) { true } - end + let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } it { is_expected.to eq(:merged_result) } end context 'when pipeline is detached merge request pipeline' do - before do - allow(pipeline).to receive(:detached_merge_request_pipeline?) { true } - end + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } it { is_expected.to eq(:detached) } end @@ -499,50 +325,6 @@ describe Ci::Pipeline, :mailer do end end - describe '.triggered_for_branch' do - subject { described_class.triggered_for_branch(ref) } - - let(:project) { create(:project, :repository) } - let(:ref) { 'feature' } - let!(:pipeline) { create(:ci_pipeline, ref: ref) } - - it 'returns the pipeline' do - is_expected.to eq([pipeline]) - end - - context 'when sha is not specified' do - it 'returns the pipeline' do - expect(described_class.triggered_for_branch(ref)).to eq([pipeline]) - end - end - - context 'when pipeline is triggered for tag' do - let(:ref) { 'v1.1.0' } - let!(:pipeline) { create(:ci_pipeline, ref: ref, tag: true) } - - it 'does not return the pipeline' do - is_expected.to be_empty - end - end - - context 'when pipeline is triggered for merge_request' do - let!(:merge_request) do - create(:merge_request, - :with_merge_request_pipeline, - source_project: project, - source_branch: ref, - target_project: project, - target_branch: 'master') - end - - let(:pipeline) { merge_request.pipelines_for_merge_request.first } - - it 'does not return the pipeline' do - is_expected.to be_empty - end - end - end - describe '.with_reports' do subject { described_class.with_reports(Ci::JobArtifact.test_reports) } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 049db4f7013..7c419a195cd 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -960,4 +960,20 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do end end end + + describe '#delete_cached_resources!' do + let!(:cluster) { create(:cluster, :project) } + let!(:staging_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, namespace: 'staging') } + let!(:production_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, namespace: 'production') } + + subject { cluster.delete_cached_resources! } + + it 'deletes associated namespace records' do + expect(cluster.kubernetes_namespaces).to match_array([staging_namespace, production_namespace]) + + subject + + expect(cluster.kubernetes_namespaces).to be_empty + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index f7bef9e71e2..4a6a9026f77 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -852,4 +852,77 @@ describe Issuable do it_behaves_like 'matches_cross_reference_regex? fails fast' end end + + describe 'release scopes' do + let_it_be(:project) { create(:project) } + + let_it_be(:release_1) { create(:release, tag: 'v1.0', project: project) } + let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project) } + let_it_be(:release_3) { create(:release, tag: 'v3.0', project: project) } + let_it_be(:release_4) { create(:release, tag: 'v4.0', project: project) } + + let_it_be(:milestone_1) { create(:milestone, releases: [release_1], title: 'm1', project: project) } + let_it_be(:milestone_2) { create(:milestone, releases: [release_1, release_2], title: 'm2', project: project) } + let_it_be(:milestone_3) { create(:milestone, releases: [release_2, release_4], title: 'm3', project: project) } + let_it_be(:milestone_4) { create(:milestone, releases: [release_3], title: 'm4', project: project) } + let_it_be(:milestone_5) { create(:milestone, releases: [release_3], title: 'm5', project: project) } + let_it_be(:milestone_6) { create(:milestone, title: 'm6', project: project) } + + let_it_be(:issue_1) { create(:issue, milestone: milestone_1, project: project) } + let_it_be(:issue_2) { create(:issue, milestone: milestone_1, project: project) } + let_it_be(:issue_3) { create(:issue, milestone: milestone_2, project: project) } + let_it_be(:issue_4) { create(:issue, milestone: milestone_5, project: project) } + let_it_be(:issue_5) { create(:issue, milestone: milestone_6, project: project) } + let_it_be(:issue_6) { create(:issue, project: project) } + + let_it_be(:items) { Issue.all } + + describe '#without_release' do + it 'returns the issues not tied to any milestone and the ones tied to milestone with no release' do + expect(items.without_release).to contain_exactly(issue_5, issue_6) + end + end + + describe '#any_release' do + it 'returns all issues tied to a release' do + expect(items.any_release).to contain_exactly(issue_1, issue_2, issue_3, issue_4) + end + end + + describe '#with_release' do + it 'returns the issues tied a specfic release' do + expect(items.with_release('v1.0', project.id)).to contain_exactly(issue_1, issue_2, issue_3) + end + + context 'when a release has a milestone with one issue and another one with no issue' do + it 'returns that one issue' do + expect(items.with_release('v2.0', project.id)).to contain_exactly(issue_3) + end + + context 'when the milestone with no issue is added as a filter' do + it 'returns an empty list' do + expect(items.with_release('v2.0', project.id).with_milestone('m3')).to be_empty + end + end + + context 'when the milestone with the issue is added as a filter' do + it 'returns this issue' do + expect(items.with_release('v2.0', project.id).with_milestone('m2')).to contain_exactly(issue_3) + end + end + end + + context 'when there is no issue under a specific release' do + it 'returns no issue' do + expect(items.with_release('v4.0', project.id)).to be_empty + end + end + + context 'when a non-existent release tag is passed in' do + it 'returns no issue' do + expect(items.with_release('v999.0', project.id)).to be_empty + end + end + end + end end diff --git a/spec/models/merge_request/pipelines_spec.rb b/spec/models/merge_request/pipelines_spec.rb new file mode 100644 index 00000000000..96f09eda647 --- /dev/null +++ b/spec/models/merge_request/pipelines_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequest::Pipelines do + describe '#all' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.source_project } + + subject { described_class.new(merge_request) } + + shared_examples 'returning pipelines with proper ordering' do + let!(:all_pipelines) do + merge_request.all_commit_shas.map do |sha| + create(:ci_empty_pipeline, + project: project, sha: sha, ref: merge_request.source_branch) + end + end + + it 'returns all pipelines' do + expect(subject.all).not_to be_empty + expect(subject.all).to eq(all_pipelines.reverse) + end + end + + context 'with single merge_request_diffs' do + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with multiple irrelevant merge_request_diffs' do + before do + merge_request.update(target_branch: 'v1.0.0') + end + + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with unsaved merge request' do + let(:merge_request) { build(:merge_request) } + + let!(:pipeline) do + create(:ci_empty_pipeline, project: project, + sha: merge_request.diff_head_sha, ref: merge_request.source_branch) + end + + it 'returns pipelines from diff_head_sha' do + expect(subject.all).to contain_exactly(pipeline) + end + end + + context 'when pipelines exist for the branch and merge request' do + let(:source_ref) { 'feature' } + let(:target_ref) { 'master' } + + let!(:branch_pipeline) do + create(:ci_pipeline, source: :push, project: project, + ref: source_ref, sha: shas.second) + end + + let!(:tag_pipeline) do + create(:ci_pipeline, project: project, ref: source_ref, tag: true) + end + + let!(:detached_merge_request_pipeline) do + create(:ci_pipeline, source: :merge_request_event, project: project, + ref: source_ref, sha: shas.second, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: source_ref, + target_project: project, target_branch: target_ref) + end + + let(:project) { create(:project, :repository) } + let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) } + + before do + allow(merge_request).to receive(:all_commit_shas) { shas } + end + + it 'returns merge request pipeline first' do + expect(subject.all).to eq([detached_merge_request_pipeline, branch_pipeline]) + end + + context 'when there are a branch pipeline and a merge request pipeline' do + let!(:branch_pipeline_2) do + create(:ci_pipeline, source: :push, project: project, + ref: source_ref, sha: shas.first) + end + + let!(:detached_merge_request_pipeline_2) do + create(:ci_pipeline, source: :merge_request_event, project: project, + ref: source_ref, sha: shas.first, merge_request: merge_request) + end + + it 'returns merge request pipelines first' do + expect(subject.all) + .to eq([detached_merge_request_pipeline_2, + detached_merge_request_pipeline, + branch_pipeline_2, + branch_pipeline]) + end + end + + context 'when there are multiple merge request pipelines from the same branch' do + let!(:branch_pipeline_2) do + create(:ci_pipeline, source: :push, project: project, + ref: source_ref, sha: shas.first) + end + + let!(:detached_merge_request_pipeline_2) do + create(:ci_pipeline, source: :merge_request_event, project: project, + ref: source_ref, sha: shas.first, merge_request: merge_request_2) + end + + let(:merge_request_2) do + create(:merge_request, source_project: project, source_branch: source_ref, + target_project: project, target_branch: 'stable') + end + + before do + allow(merge_request_2).to receive(:all_commit_shas) { shas } + end + + it 'returns only related merge request pipelines' do + expect(subject.all) + .to eq([detached_merge_request_pipeline, + branch_pipeline_2, + branch_pipeline]) + + expect(described_class.new(merge_request_2).all) + .to eq([detached_merge_request_pipeline_2, + branch_pipeline_2, + branch_pipeline]) + end + end + + context 'when detached merge request pipeline is run on head ref of the merge request' do + let!(:detached_merge_request_pipeline) do + create(:ci_pipeline, source: :merge_request_event, project: project, + ref: merge_request.ref_path, sha: shas.second, merge_request: merge_request) + end + + it 'sets the head ref of the merge request to the pipeline ref' do + expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head}) + end + + it 'includes the detached merge request pipeline even though the ref is custom path' do + expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline) + end + end + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 656753da5f1..53d67113f9e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1441,183 +1441,6 @@ describe MergeRequest do end end - describe '#all_pipelines' do - shared_examples 'returning pipelines with proper ordering' do - let!(:all_pipelines) do - subject.all_commit_shas.map do |sha| - create(:ci_empty_pipeline, - project: subject.source_project, - sha: sha, - ref: subject.source_branch) - end - end - - it 'returns all pipelines' do - expect(subject.all_pipelines).not_to be_empty - expect(subject.all_pipelines).to eq(all_pipelines.reverse) - end - end - - context 'with single merge_request_diffs' do - it_behaves_like 'returning pipelines with proper ordering' - end - - context 'with multiple irrelevant merge_request_diffs' do - before do - subject.update(target_branch: 'v1.0.0') - end - - it_behaves_like 'returning pipelines with proper ordering' - end - - context 'with unsaved merge request' do - subject { build(:merge_request) } - - let!(:pipeline) do - create(:ci_empty_pipeline, - project: subject.project, - sha: subject.diff_head_sha, - ref: subject.source_branch) - end - - it 'returns pipelines from diff_head_sha' do - expect(subject.all_pipelines).to contain_exactly(pipeline) - end - end - - context 'when pipelines exist for the branch and merge request' do - let(:source_ref) { 'feature' } - let(:target_ref) { 'master' } - - let!(:branch_pipeline) do - create(:ci_pipeline, - source: :push, - project: project, - ref: source_ref, - sha: shas.second) - end - - let!(:detached_merge_request_pipeline) do - create(:ci_pipeline, - source: :merge_request_event, - project: project, - ref: source_ref, - sha: shas.second, - merge_request: merge_request) - end - - let(:merge_request) do - create(:merge_request, - source_project: project, - source_branch: source_ref, - target_project: project, - target_branch: target_ref) - end - - let(:project) { create(:project, :repository) } - let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) } - - before do - allow(merge_request).to receive(:all_commit_shas) { shas } - end - - it 'returns merge request pipeline first' do - expect(merge_request.all_pipelines) - .to eq([detached_merge_request_pipeline, - branch_pipeline]) - end - - context 'when there are a branch pipeline and a merge request pipeline' do - let!(:branch_pipeline_2) do - create(:ci_pipeline, - source: :push, - project: project, - ref: source_ref, - sha: shas.first) - end - - let!(:detached_merge_request_pipeline_2) do - create(:ci_pipeline, - source: :merge_request_event, - project: project, - ref: source_ref, - sha: shas.first, - merge_request: merge_request) - end - - it 'returns merge request pipelines first' do - expect(merge_request.all_pipelines) - .to eq([detached_merge_request_pipeline_2, - detached_merge_request_pipeline, - branch_pipeline_2, - branch_pipeline]) - end - end - - context 'when there are multiple merge request pipelines from the same branch' do - let!(:branch_pipeline_2) do - create(:ci_pipeline, - source: :push, - project: project, - ref: source_ref, - sha: shas.first) - end - - let!(:detached_merge_request_pipeline_2) do - create(:ci_pipeline, - source: :merge_request_event, - project: project, - ref: source_ref, - sha: shas.first, - merge_request: merge_request_2) - end - - let(:merge_request_2) do - create(:merge_request, - source_project: project, - source_branch: source_ref, - target_project: project, - target_branch: 'stable') - end - - before do - allow(merge_request_2).to receive(:all_commit_shas) { shas } - end - - it 'returns only related merge request pipelines' do - expect(merge_request.all_pipelines) - .to eq([detached_merge_request_pipeline, - branch_pipeline_2, - branch_pipeline]) - - expect(merge_request_2.all_pipelines) - .to eq([detached_merge_request_pipeline_2, - branch_pipeline_2, - branch_pipeline]) - end - end - - context 'when detached merge request pipeline is run on head ref of the merge request' do - let!(:detached_merge_request_pipeline) do - create(:ci_pipeline, - source: :merge_request_event, - project: project, - ref: merge_request.ref_path, - sha: shas.second, - merge_request: merge_request) - end - - it 'sets the head ref of the merge request to the pipeline ref' do - expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head}) - end - - it 'includes the detached merge request pipeline even though the ref is custom path' do - expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline) - end - end - end - end - describe '#update_head_pipeline' do subject { merge_request.update_head_pipeline } diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index bc22818ede7..73748b39922 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -262,4 +262,28 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do end end end + + describe '#track_events after_commit callback' do + before do + allow(service).to receive(:prometheus_available?).and_return(true) + end + + context "enabling manual_configuration" do + it "tracks enable event" do + service.update!(manual_configuration: false) + + expect(Gitlab::Tracking).to receive(:event).with('cluster:services:prometheus', 'enabled_manual_prometheus') + + service.update!(manual_configuration: true) + end + + it "tracks disable event" do + service.update!(manual_configuration: true) + + expect(Gitlab::Tracking).to receive(:event).with('cluster:services:prometheus', 'disabled_manual_prometheus') + + service.update!(manual_configuration: false) + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 92450bfdaff..9cb3229aeb1 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -66,14 +66,16 @@ describe Repository do end describe 'tags_sorted_by' do + let(:tags_to_compare) { %w[v1.0.0 v1.1.0] } + context 'name_desc' do - subject { repository.tags_sorted_by('name_desc').map(&:name) } + subject { repository.tags_sorted_by('name_desc').map(&:name) & tags_to_compare } it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } end context 'name_asc' do - subject { repository.tags_sorted_by('name_asc').map(&:name) } + subject { repository.tags_sorted_by('name_asc').map(&:name) & tags_to_compare } it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } end @@ -115,7 +117,7 @@ describe Repository do context 'annotated tag pointing to a blob' do let(:annotated_tag_name) { 'annotated-tag' } - subject { repository.tags_sorted_by('updated_asc').map(&:name) } + subject { repository.tags_sorted_by('updated_asc').map(&:name) & (tags_to_compare + [annotated_tag_name]) } before do options = { message: 'test tag message\n', diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb index 11a8decc9cc..d40ca856f7b 100644 --- a/spec/presenters/group_clusterable_presenter_spec.rb +++ b/spec/presenters/group_clusterable_presenter_spec.rb @@ -83,6 +83,12 @@ describe GroupClusterablePresenter do it { is_expected.to eq(update_applications_group_cluster_path(group, cluster, application)) } end + describe '#clear_cluster_cache_path' do + subject { presenter.clear_cluster_cache_path(cluster) } + + it { is_expected.to eq(clear_cache_group_cluster_path(group, cluster)) } + end + describe '#cluster_path' do subject { presenter.cluster_path(cluster) } diff --git a/spec/presenters/instance_clusterable_presenter_spec.rb b/spec/presenters/instance_clusterable_presenter_spec.rb index 9f1268379f5..3e7ee7a0ff6 100644 --- a/spec/presenters/instance_clusterable_presenter_spec.rb +++ b/spec/presenters/instance_clusterable_presenter_spec.rb @@ -34,4 +34,10 @@ describe InstanceClusterablePresenter do it { is_expected.to eq(aws_proxy_admin_clusters_path(resource: resource)) } end + + describe '#clear_cluster_cache_path' do + subject { presenter.clear_cluster_cache_path(cluster) } + + it { is_expected.to eq(clear_cache_admin_cluster_path(cluster)) } + end end diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index 441c2a50fea..b3dad4abde5 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -83,6 +83,12 @@ describe ProjectClusterablePresenter do it { is_expected.to eq(update_applications_project_cluster_path(project, cluster, application)) } end + describe '#clear_cluster_cache_path' do + subject { presenter.clear_cluster_cache_path(cluster) } + + it { is_expected.to eq(clear_cache_project_cluster_path(project, cluster)) } + end + describe '#cluster_path' do subject { presenter.cluster_path(cluster) } diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb index 7ab454c4ff0..2f978b0a036 100644 --- a/spec/presenters/release_presenter_spec.rb +++ b/spec/presenters/release_presenter_spec.rb @@ -90,14 +90,6 @@ describe ReleasePresenter do is_expected.to match /#{edit_project_release_url(project, release)}/ end - context 'when release_edit_page feature flag is disabled' do - before do - stub_feature_flags(release_edit_page: false) - end - - it { is_expected.to be_nil } - end - context 'when a user is not allowed to update a release' do let(:presenter) { described_class.new(release, current_user: guest) } diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb index ea0a7d4c9b7..d931dea01e7 100644 --- a/spec/requests/api/badges_spec.rb +++ b/spec/requests/api/badges_spec.rb @@ -81,6 +81,7 @@ describe API::Badges do get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user) expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq(badge.name) expect(json_response['id']).to eq(badge.id) expect(json_response['link_url']).to eq(badge.link_url) expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url) @@ -98,6 +99,7 @@ describe API::Badges do include_context 'source helpers' let(:source) { get_source(source_type) } + let(:example_name) { 'BadgeName' } let(:example_url) { 'http://www.example.com' } let(:example_url2) { 'http://www.example1.com' } @@ -105,7 +107,7 @@ describe API::Badges do it_behaves_like 'a 404 response when source is private' do let(:route) do post api("/#{source_type.pluralize}/#{source.id}/badges", stranger), - params: { link_url: example_url, image_url: example_url2 } + params: { name: example_name, link_url: example_url, image_url: example_url2 } end end @@ -128,11 +130,12 @@ describe API::Badges do it 'creates a new badge' do expect do post api("/#{source_type.pluralize}/#{source.id}/badges", maintainer), - params: { link_url: example_url, image_url: example_url2 } + params: { name: example_name, link_url: example_url, image_url: example_url2 } expect(response).to have_gitlab_http_status(201) end.to change { source.badges.count }.by(1) + expect(json_response['name']).to eq(example_name) expect(json_response['link_url']).to eq(example_url) expect(json_response['image_url']).to eq(example_url2) expect(json_response['kind']).to eq source_type @@ -169,6 +172,7 @@ describe API::Badges do context "with :sources == #{source_type.pluralize}" do let(:badge) { source.badges.first } + let(:example_name) { 'BadgeName' } let(:example_url) { 'http://www.example.com' } let(:example_url2) { 'http://www.example1.com' } @@ -197,9 +201,10 @@ describe API::Badges do context 'when authenticated as a maintainer/owner' do it 'updates the member', :quarantine do put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer), - params: { link_url: example_url, image_url: example_url2 } + params: { name: example_name, link_url: example_url, image_url: example_url2 } expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq(example_name) expect(json_response['link_url']).to eq(example_url) expect(json_response['image_url']).to eq(example_url2) expect(json_response['kind']).to eq source_type @@ -297,7 +302,7 @@ describe API::Badges do expect(response).to have_gitlab_http_status(200) - expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url') + expect(json_response.keys).to contain_exactly('name', 'link_url', 'rendered_link_url', 'image_url', 'rendered_image_url') expect(json_response['link_url']).to eq(example_url) expect(json_response['image_url']).to eq(example_url2) expect(json_response['rendered_link_url']).to eq(example_url) @@ -351,9 +356,9 @@ describe API::Badges do project.add_developer(developer) project.add_maintainer(maintainer) project.request_access(access_requester) - project.project_badges << build(:project_badge, project: project) - project.project_badges << build(:project_badge, project: project) - project_group.badges << build(:group_badge, group: group) + project.project_badges << build(:project_badge, project: project, name: 'ExampleBadge1') + project.project_badges << build(:project_badge, project: project, name: 'ExampleBadge2') + project_group.badges << build(:group_badge, group: group, name: 'ExampleBadge3') end end @@ -362,8 +367,8 @@ describe API::Badges do group.add_developer(developer) group.add_owner(maintainer) group.request_access(access_requester) - group.badges << build(:group_badge, group: group) - group.badges << build(:group_badge, group: group) + group.badges << build(:group_badge, group: group, name: 'ExampleBadge4') + group.badges << build(:group_badge, group: group, name: 'ExampleBadge5') end end end diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb new file mode 100644 index 00000000000..1efa9e16233 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Setting Due Date of an issue' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:input) { { due_date: 2.days.since } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: issue.iid.to_s + } + graphql_mutation(:issue_set_due_date, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + dueDate + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_due_date) + end + + before do + project.add_developer(current_user) + end + + it 'returns an error if the user is not allowed to update the issue' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + + it 'updates the issue due date' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']['dueDate']).to eq(2.days.since.to_date.to_s) + end + + context 'when passing due date without a date value' do + let(:input) { { due_date: 'test' } } + + it 'returns internal server error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to include(a_hash_including('message' => 'Internal server error')) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f1447536e0f..cda2dd7d2f4 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -49,6 +49,8 @@ shared_examples 'languages and percentages JSON response' do end describe API::Projects do + include ProjectForksHelper + let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -1163,6 +1165,18 @@ describe API::Projects do expect(json_response.keys).not_to include('permissions') end + context 'the project is a public fork' do + it 'hides details of a public fork parent' do + public_project = create(:project, :repository, :public) + fork = fork_project(public_project) + + get api("/projects/#{fork.id}") + + expect(response).to have_gitlab_http_status(200) + expect(json_response['forked_from_project']).to be_nil + end + end + context 'and the project has a private repository' do let(:project) { create(:project, :repository, :public, :repository_private) } let(:protected_attributes) { %w(default_branch ci_config_path) } @@ -1479,6 +1493,28 @@ describe API::Projects do end end + context 'the project is a fork' do + it 'shows details of a visible fork parent' do + fork = fork_project(project, user) + + get api("/projects/#{fork.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['forked_from_project']).to include('id' => project.id) + end + + it 'hides details of a hidden fork parent' do + fork = fork_project(project, user) + fork_user = create(:user) + fork.team.add_developer(fork_user) + + get api("/projects/#{fork.id}", fork_user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['forked_from_project']).to be_nil + end + end + describe 'permissions' do context 'all projects' do before do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 3c6ec631664..dca87d5e4ce 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -7,6 +7,7 @@ describe API::Tags do let(:guest) { create(:user).tap { |u| project.add_guest(u) } } let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:tag_name) { project.repository.find_tag('v1.1.0').name } + let(:tag_message) { project.repository.find_tag('v1.1.0').message } let(:project_id) { project.id } let(:current_user) { nil } @@ -75,7 +76,7 @@ describe API::Tags do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/tags') expect(response).to include_pagination_headers - expect(json_response.first['name']).to eq(tag_name) + expect(json_response.map { |r| r['name'] }).to include(tag_name) end context 'when repository is disabled' do @@ -135,9 +136,10 @@ describe API::Tags do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/tags') expect(response).to include_pagination_headers - expect(json_response.first['name']).to eq(tag_name) - expect(json_response.first['message']).to eq('Version 1.1.0') - expect(json_response.first['release']['description']).to eq(description) + + expected_tag = json_response.find { |r| r['name'] == tag_name } + expect(expected_tag['message']).to eq(tag_message) + expect(expected_tag['release']['description']).to eq(description) end end end diff --git a/spec/rubocop/cop/graphql/authorize_types_spec.rb b/spec/rubocop/cop/graphql/authorize_types_spec.rb index af4315ecd34..98797a780e0 100644 --- a/spec/rubocop/cop/graphql/authorize_types_spec.rb +++ b/spec/rubocop/cop/graphql/authorize_types_spec.rb @@ -79,5 +79,15 @@ describe RuboCop::Cop::Graphql::AuthorizeTypes do end TYPE end + + it 'does not add an offense for Enums' do + expect_no_offenses(<<~TYPE) + module Types + class ATypeEnum < AnotherEnum + field :a_thing + end + end + TYPE + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index de0f4841215..8f16d375f03 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -801,6 +801,32 @@ describe Ci::CreatePipelineService do end end + context 'environment with Kubernetes configuration' do + let(:kubernetes_namespace) { 'custom-namespace' } + + before do + config = YAML.dump( + deploy: { + environment: { + name: "environment-name", + kubernetes: { namespace: kubernetes_namespace } + }, + script: 'ls' + } + ) + + stub_ci_pipeline_yaml_file(config) + end + + it 'stores the requested namespace' do + result = execute_service + build = result.builds.first + + expect(result).to be_persisted + expect(build.options.dig(:environment, :kubernetes, :namespace)).to eq(kubernetes_namespace) + end + end + context 'when environment with invalid name' do before do config = YAML.dump(deploy: { environment: { name: 'name,with,commas' }, script: 'ls' }) diff --git a/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb b/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb index 80120629a32..18d025a4b07 100644 --- a/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb +++ b/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb @@ -3,6 +3,10 @@ # This pending test can be removed when `single_mr_diff_view` is enabled by default # disabling the feature flag above is then not needed anymore. RSpec.shared_examples 'rendering a single diff version' do |attribute| + before do + stub_feature_flags(diffs_batch_load: false) + end + pending 'allows editing diff settings single_mr_diff_view is enabled' do project = create(:project, :repository) user = project.creator diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 4d5b369e88e..9956144b601 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe 'projects/_home_panel' do + include ProjectForksHelper + context 'notifications' do let(:project) { create(:project) } @@ -144,4 +146,36 @@ describe 'projects/_home_panel' do end end end + + context 'forks' do + let(:source_project) { create(:project, :repository) } + let(:project) { fork_project(source_project) } + let(:user) { create(:user) } + + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + end + + context 'user can read fork source' do + it 'shows the forked-from project' do + allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(true) + + render + + expect(rendered).to have_content("Forked from #{source_project.full_name}") + end + end + + context 'user cannot read fork source' do + it 'does not show the forked-from project' do + allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(false) + + render + + expect(rendered).to have_content("Forked from an inaccessible project") + end + end + end end diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index f576093ab45..40927a22dc4 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe 'projects/edit' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:project) { create(:project) } let(:user) { create(:admin) } @@ -26,4 +27,59 @@ describe 'projects/edit' do expect(rendered).not_to have_content('Export project') end end + + context 'forking' do + before do + assign(:project, project) + + allow(view).to receive(:current_user).and_return(user) + end + + context 'project is not a fork' do + it 'hides the remove fork relationship settings' do + render + + expect(rendered).not_to have_content('Remove fork relationship') + end + end + + context 'project is a fork' do + let(:source_project) { create(:project) } + let(:project) { fork_project(source_project) } + + it 'shows the remove fork relationship settings to an authorized user' do + allow(view).to receive(:can?).with(user, :remove_fork_project, project).and_return(true) + + render + + expect(rendered).to have_content('Remove fork relationship') + end + + it 'hides the fork relationship settings from an unauthorized user' do + allow(view).to receive(:can?).with(user, :remove_fork_project, project).and_return(false) + + render + + expect(rendered).not_to have_content('Remove fork relationship') + end + + it 'hides the fork source from an unauthorized user' do + allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(false) + + render + + expect(rendered).to have_content('Remove fork relationship') + expect(rendered).not_to have_content(source_project.full_name) + end + + it 'shows the fork source to an authorized user' do + allow(view).to receive(:can?).with(user, :read_project, source_project).and_return(true) + + render + + expect(rendered).to have_content('Remove fork relationship') + expect(rendered).to have_content(source_project.full_name) + end + end + end end diff --git a/yarn.lock b/yarn.lock index 4f66345fed0..9414587c3a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -722,10 +722,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.82.0.tgz#c059c460afc13ebfe9df370521ca8963fa5afb80" integrity sha512-9L4Brys2LCk44lHvFsCFDKN768lYjoMVYDb4PD7FSjqUEruQQ1SRj0rvb1RWKLhiTCDKrtDOXkH6I1TTEms24w== -"@gitlab/ui@7.15.2": - version "7.15.2" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-7.15.2.tgz#924c202ea43ad79032d91d803665b1f7b8f0a42e" - integrity sha512-XNrs2iH8waHk/LDp3sTUSlq3vASHUL4WwCiKwoPJP7PZyXZvvumrkNmiDS0ZvPRPB3ZvIrSywRf61sL0PiQZEA== +"@gitlab/ui@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-7.16.1.tgz#a539bd2e39866549f71d8678efe7cca8478ebde3" + integrity sha512-7SdwSC2P2/PKZNaIzNihAudSpP95cex98i6IMcukK0ocJYvHr8S9s8GoznaD8YugTR1EGhu+f1M6ubneU5vUwQ== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" @@ -970,10 +970,10 @@ "@sentry/types" "5.7.1" tslib "^1.9.3" -"@sourcegraph/code-host-integration@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.13.tgz#4fd5fe1e0088c63b2a26be231c5a2a4ca79b1596" - integrity sha512-IjF9gb9e8dG8p12DKg5Z7UMOVQO/ClH3AyMCPfX/qH7DH/0b55WH6stYVqZu6y776quFonO4Z9gWYM8pQZjzKw== +"@sourcegraph/code-host-integration@^0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.14.tgz#e12b08371dc37bf4a468450b008c6e167705e1a8" + integrity sha512-S4+K+3RKFd49Btl1D9LOdWXROgXevUwOBwp+vDUuGgzT2d6Y+qjalUJ0t8CjbYzdBdJun+2/Zi1+SXfm+S+xVg== "@types/anymatch@*": version "1.3.0" @@ -3213,17 +3213,17 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1: +d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw== -d3-axis@1, d3-axis@1.0.8, d3-axis@^1.0.8: +d3-axis@1, d3-axis@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" integrity sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo= -d3-brush@1, d3-brush@1.0.4, d3-brush@^1.0.4: +d3-brush@1, d3-brush@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4" integrity sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q= @@ -3281,7 +3281,7 @@ d3-dsv@1, d3-dsv@1.0.8: iconv-lite "0.4" rw "1" -d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3: +d3-ease@1, d3-ease@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4= @@ -3400,21 +3400,21 @@ d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== -d3-shape@1, d3-shape@1.2.0, d3-shape@^1.2.0: +d3-shape@1, d3-shape@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777" integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c= dependencies: d3-path "1" -d3-time-format@2, d3-time-format@2.1.1, d3-time-format@^2.1.1: +d3-time-format@2, d3-time-format@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31" integrity sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw== dependencies: d3-time "1" -d3-time@1, d3-time@1.0.8, d3-time@^1.0.8: +d3-time@1, d3-time@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84" integrity sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ== @@ -3424,7 +3424,7 @@ d3-timer@1, d3-timer@1.0.7: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531" integrity sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA== -d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1: +d3-transition@1, d3-transition@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039" integrity sha512-xeg8oggyQ+y5eb4J13iDgKIjUcEfIOZs2BqV/eEmXm2twx80wTzJ4tB4vaZ5BKfz7XsI/DFmQL5me6O27/5ykQ== |