diff options
author | Yorick Peterse <yorickpeterse@gmail.com> | 2019-02-08 14:34:16 +0100 |
---|---|---|
committer | Yorick Peterse <yorickpeterse@gmail.com> | 2019-02-08 14:34:16 +0100 |
commit | 6afe2688aafa31347eea245b8e98577c80bbd443 (patch) | |
tree | 53a7b6c57eb5560cf8ed76466762a2225cbae824 | |
parent | 5b4c5e84762b09529838c466b12a13fae024457f (diff) | |
parent | 7444d7e4b3a5bf217f4e0041e4a80c63da1a645e (diff) | |
download | gitlab-ce-6afe2688aafa31347eea245b8e98577c80bbd443.tar.gz |
Merge master to 11.8 stable for the feature freeze
321 files changed, 5107 insertions, 1524 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b93a79de994..4e8453726a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -388,11 +388,13 @@ flaky-examples-check: .assets-compile-cache: &assets-compile-cache cache: - key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v3" + key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v4" paths: - vendor/ruby/ - .yarn-cache/ - - tmp/cache/assets/sprockets + # We have disabled caching of sprockets for now, as it fails to pick up changes in SCSS: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/57431 + # - tmp/cache/assets/sprockets compile-assets: <<: *dedicated-runner @@ -636,7 +638,6 @@ gitlab:assets:compile: image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 dependencies: - setup-test-env - - compile-assets services: - docker:stable-dind variables: diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 092afa15df4..815d5ca06d5 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.17.0 +1.19.0 @@ -422,7 +422,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.5.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly' gem 'grpc', '~> 1.15.0' gem 'google-protobuf', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index f661da41507..0b2bd2c96bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -278,7 +278,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.5.0) + gitaly-proto (1.10.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -1020,7 +1020,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.5.0) + gitaly-proto (~> 1.10.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index cace8bb9dba..73ce3e760ab 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -437,7 +437,7 @@ export class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` - <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> + <button class="btn award-control js-emoji-btn has-tooltip active" title="You"> ${this.emoji.glEmojiTag(emojiName)} <span class="award-control-text js-counter">1</span> </button> diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index fc4779632f9..6ebd1ad109e 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -6,7 +6,13 @@ import Flash from '../flash'; import Poll from '../lib/utils/poll'; import initSettingsPanels from '../settings_panels'; import eventHub from './event_hub'; -import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from './constants'; +import { + APPLICATION_STATUS, + REQUEST_SUBMITTED, + REQUEST_FAILURE, + UPGRADE_REQUESTED, + UPGRADE_REQUEST_FAILURE, +} from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; @@ -120,11 +126,17 @@ export default class Clusters { addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); + eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); + eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId)); + eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); } removeListeners() { if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); + eventHub.$off('upgradeApplication', this.upgradeApplication); + eventHub.$off('upgradeFailed', this.upgradeFailed); + eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); } initPolling() { @@ -245,6 +257,21 @@ export default class Clusters { }); } + upgradeApplication(data) { + const appId = data.id; + this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED); + this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); + this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId)); + } + + upgradeFailed(appId) { + this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE); + } + + dismissUpgradeSuccess(appId) { + this.store.updateAppProperty(appId, 'requestStatus', null); + } + destroy() { this.destroyed = true; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 3c3ce1dec56..5952e93b9a7 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,15 +1,24 @@ <script> /* eslint-disable vue/require-default-prop */ +import { GlLink } from '@gitlab/ui'; +import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '../constants'; +import { + APPLICATION_STATUS, + REQUEST_SUBMITTED, + REQUEST_FAILURE, + UPGRADE_REQUESTED, +} from '../constants'; export default { components: { loadingButton, identicon, + TimeagoTooltip, + GlLink, }, props: { id: { @@ -54,6 +63,18 @@ export default { type: String, required: false, }, + version: { + type: String, + required: false, + }, + chartRepo: { + type: String, + required: false, + }, + upgradeAvailable: { + type: Boolean, + required: false, + }, installApplicationRequestParams: { type: Object, required: false, @@ -78,7 +99,8 @@ export default { return ( this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED || - this.status === APPLICATION_STATUS.UPDATING + this.status === APPLICATION_STATUS.UPDATING || + this.status === APPLICATION_STATUS.UPDATE_ERRORED ); }, canInstall() { @@ -146,6 +168,69 @@ export default { title: this.title, }); }, + versionLabel() { + if (this.upgradeFailed) { + return s__('ClusterIntegration|Upgrade failed'); + } else if (this.isUpgrading) { + return s__('ClusterIntegration|Upgrading'); + } + + return s__('ClusterIntegration|Upgraded'); + }, + upgradeRequested() { + return this.requestStatus === UPGRADE_REQUESTED; + }, + upgradeSuccessful() { + return this.status === APPLICATION_STATUS.UPDATED; + }, + upgradeFailed() { + if (this.isUpgrading) { + return false; + } + + return this.status === APPLICATION_STATUS.UPDATE_ERRORED; + }, + upgradeFailureDescription() { + return sprintf( + s__( + 'ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again.', + ), + { + title: this.title, + }, + ); + }, + upgradeSuccessDescription() { + return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { + title: this.title, + }); + }, + upgradeButtonLabel() { + let label; + if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) { + label = s__('ClusterIntegration|Upgrade'); + } else if (this.isUpgrading) { + label = s__('ClusterIntegration|Upgrading'); + } else if (this.upgradeFailed) { + label = s__('ClusterIntegration|Retry upgrade'); + } + + return label; + }, + isUpgrading() { + // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend + return ( + this.status === APPLICATION_STATUS.UPDATING || + (this.upgradeRequested && !this.upgradeSuccessful) + ); + }, + }, + watch: { + status() { + if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) { + eventHub.$emit('upgradeFailed', this.id); + } + }, }, methods: { installClicked() { @@ -154,6 +239,15 @@ export default { params: this.installApplicationRequestParams, }); }, + upgradeClicked() { + eventHub.$emit('upgradeApplication', { + id: this.id, + params: this.installApplicationRequestParams, + }); + }, + dismissUpgradeSuccess() { + eventHub.$emit('dismissUpgradeSuccess', this.id); + }, }, }; </script> @@ -207,6 +301,51 @@ export default { </li> </ul> </div> + + <div + v-if="(upgradeSuccessful || upgradeFailed) && !upgradeAvailable" + class="form-text text-muted label p-0 js-cluster-application-upgrade-details" + > + {{ versionLabel }} + + <span v-if="upgradeSuccessful"> to</span> + + <gl-link + v-if="upgradeSuccessful" + :href="chartRepo" + target="_blank" + class="js-cluster-application-upgrade-version" + > + chart v{{ version }} + </gl-link> + </div> + + <div + v-if="upgradeFailed && !isUpgrading" + class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" + > + {{ upgradeFailureDescription }} + </div> + + <div + v-if="upgradeRequested && upgradeSuccessful" + class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3" + > + {{ upgradeSuccessDescription }} + + <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess"> + × + </button> + </div> + + <loading-button + v-if="upgradeAvailable || upgradeFailed || isUpgrading" + class="btn btn-primary js-cluster-application-upgrade-button mt-2" + :loading="isUpgrading" + :disabled="isUpgrading" + :label="upgradeButtonLabel" + @click="upgradeClicked" + /> </div> <div :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }" diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 5d19c79570a..0cf187d4189 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -362,6 +362,9 @@ export default { :status-reason="applications.runner.statusReason" :request-status="applications.runner.requestStatus" :request-reason="applications.runner.requestReason" + :version="applications.runner.version" + :chart-repo="applications.runner.chartRepo" + :upgrade-available="applications.runner.upgradeAvailable" :disabled="!helmInstalled" title-link="https://docs.gitlab.com/runner/" > diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 360511e8882..39022879d91 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -12,15 +12,19 @@ export const APPLICATION_STATUS = { SCHEDULED: 'scheduled', INSTALLING: 'installing', INSTALLED: 'installed', - UPDATED: 'updated', UPDATING: 'updating', + UPDATED: 'updated', + UPDATE_ERRORED: 'update_errored', ERROR: 'errored', }; // These are only used client-side export const REQUEST_SUBMITTED = 'request-submitted'; export const REQUEST_FAILURE = 'request-failure'; +export const UPGRADE_REQUESTED = 'upgrade-requested'; +export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure'; export const INGRESS = 'ingress'; export const JUPYTER = 'jupyter'; export const KNATIVE = 'knative'; +export const RUNNER = 'runner'; export const CERT_MANAGER = 'cert_manager'; diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 8f74be4e0e6..d309678be27 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,6 +1,6 @@ import { s__ } from '../../locale'; import { parseBoolean } from '../../lib/utils/common_utils'; -import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants'; +import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants'; export default class ClusterStore { constructor() { @@ -40,6 +40,9 @@ export default class ClusterStore { statusReason: null, requestStatus: null, requestReason: null, + version: null, + chartRepo: 'https://gitlab.com/charts/gitlab-runner', + upgradeAvailable: null, }, prometheus: { title: s__('ClusterIntegration|Prometheus'), @@ -100,7 +103,13 @@ export default class ClusterStore { this.state.statusReason = serverState.status_reason; serverState.applications.forEach(serverAppEntry => { - const { name: appId, status, status_reason: statusReason } = serverAppEntry; + const { + name: appId, + status, + status_reason: statusReason, + version, + update_available: upgradeAvailable, + } = serverAppEntry; this.state.applications[appId] = { ...(this.state.applications[appId] || {}), @@ -124,6 +133,9 @@ export default class ClusterStore { serverAppEntry.hostname || this.state.applications.knative.hostname; this.state.applications.knative.externalIp = serverAppEntry.external_ip || this.state.applications.knative.externalIp; + } else if (appId === RUNNER) { + this.state.applications.runner.version = version; + this.state.applications.runner.upgradeAvailable = upgradeAvailable; } }); } diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 3ef54752436..0bf2dde8b96 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -6,6 +6,7 @@ import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -14,6 +15,7 @@ export default { GlLink, GlButton, SettingsDropdown, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,8 +37,15 @@ export default { }, }, computed: { - ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']), - ...mapGetters('diffs', ['hasCollapsedFile']), + ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']), + ...mapState('diffs', [ + 'commit', + 'showTreeList', + 'startVersion', + 'latestVersionPath', + 'addedLines', + 'removedLines', + ]), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, @@ -104,6 +113,11 @@ export default { <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> </div> <div class="inline-parallel-buttons d-none d-md-flex ml-auto"> + <diff-stats + :diff-files-length="diffFilesLength" + :added-lines="addedLines" + :removed-lines="removedLines" + /> <gl-button v-if="commit || startVersion" :href="latestVersionPath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index b58f704bebb..60586d4a607 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; import EditButton from './edit_button.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -16,6 +17,7 @@ export default { EditButton, Icon, FileIcon, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -202,6 +204,7 @@ export default { v-if="!diffFile.submodule && addMergeRequestButtons" class="file-actions d-none d-sm-block" > + <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <template v-if="diffFile.blob && diffFile.blob.readable_text"> <button :disabled="!diffHasDiscussions(diffFile)" diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue new file mode 100644 index 00000000000..2e5855380af --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -0,0 +1,52 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import { n__ } from '~/locale'; + +export default { + components: { Icon }, + props: { + addedLines: { + type: Number, + required: true, + }, + removedLines: { + type: Number, + required: true, + }, + diffFilesLength: { + type: Number, + required: false, + default: null, + }, + }, + computed: { + filesText() { + return n__('File', 'Files', this.diffFilesLength); + }, + isCompareVersionsHeader() { + return Boolean(this.diffFilesLength); + }, + }, +}; +</script> + +<template> + <div + class="diff-stats" + :class="{ + 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader, + 'd-inline-flex': !isCompareVersionsHeader, + }" + > + <div v-if="diffFilesLength !== null" class="diff-stats-group"> + <icon name="doc-code" class="diff-stats-icon text-secondary" /> + <strong>{{ diffFilesLength }} {{ filesText }}</strong> + </div> + <div class="diff-stats-group cgreen"> + <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong> + </div> + <div class="diff-stats-group cred"> + <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index a0f09932593..96ae197d8b8 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -14,8 +14,8 @@ export default { FileRow, }, computed: { - ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), - ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), + ...mapState('diffs', ['tree', 'renderTreeList']), + ...mapGetters('diffs', ['allBlobs']), filteredTreeList() { return this.renderTreeList ? this.tree : this.allBlobs; }, @@ -64,13 +64,6 @@ export default { {{ s__('MergeRequest|No files found') }} </p> </div> - <div v-once class="pt-3 pb-3 text-center"> - {{ n__('%d changed file', '%d changed files', diffFilesLength) }} - <div> - <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span> - <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span> - </div> - </div> </div> </template> diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 6ee33d9fc6d..47f78a5db54 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, + addedLines: null, + removedLines: null, endpoint: '', basePath: '', commit: null, diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index dbfcf8cc921..cb1b1173190 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -64,6 +64,7 @@ class DueDateSelect { this.saveDueDate(true); } }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($dueDateInput.val())); @@ -183,6 +184,7 @@ export default class DueDateSelectors { onSelect(dateText) { $datePicker.val(calendar.toString(dateText)); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate(datePickerVal)); diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index bd402c0eea5..6ece8b92a30 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -22,10 +22,6 @@ export default { type: Object, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -51,11 +47,7 @@ export default { <slot name="emptyState"></slot> <div v-if="!isLoading && environments.length > 0" class="table-holder"> - <environment-table - :environments="environments" - :can-create-deployment="canCreateDeployment" - :can-read-environment="canReadEnvironment" - /> + <environment-table :environments="environments" :can-read-environment="canReadEnvironment" /> <table-pagination v-if="pagination && pagination.totalPages > 1" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index f44806d82a6..503c1b38f71 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -47,12 +47,6 @@ export default { default: () => ({}), }, - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - canReadEnvironment: { type: Boolean, required: false, @@ -151,7 +145,7 @@ export default { }, actions() { - if (!this.model || !this.model.last_deployment || !this.canCreateDeployment) { + if (!this.model || !this.model.last_deployment) { return []; } @@ -561,7 +555,7 @@ export default { /> <rollback-component - v-if="canRetry && canCreateDeployment" + v-if="canRetry" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" /> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 87c1d44dd40..aa2417d3194 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -24,10 +24,6 @@ export default { type: Boolean, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -106,7 +102,6 @@ export default { :is-loading="isLoading" :environments="state.environments" :pagination="state.paginationInformation" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" > diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 75bdf87137f..e2c304de00a 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -23,12 +23,6 @@ export default { required: false, default: false, }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, }, methods: { folderUrl(model) { @@ -64,7 +58,6 @@ export default { is="environment-item" :key="`environment-item-${i}`" :model="model" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" /> @@ -79,7 +72,6 @@ export default { v-for="(children, index) in model.children" :key="`env-item-${i}-${index}`" :model="children" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" /> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 982e550e73c..56e7f69cad6 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -18,7 +18,6 @@ export default () => endpoint: environmentsData.environmentsDataEndpoint, folderName: environmentsData.environmentsDataFolderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment), canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment), }; }, @@ -28,7 +27,6 @@ export default () => endpoint: this.endpoint, folderName: this.folderName, cssContainerClass: this.cssContainerClass, - canCreateDeployment: this.canCreateDeployment, canReadEnvironment: this.canReadEnvironment, }, }); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index d6f0b6115a6..80f0e00400b 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -23,10 +23,6 @@ export default { type: String, required: true, }, - canCreateDeployment: { - type: Boolean, - required: true, - }, canReadEnvironment: { type: Boolean, required: true, @@ -55,7 +51,6 @@ export default { :is-loading="isLoading" :environments="state.environments" :pagination="state.paginationInformation" - :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" /> diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index d366e7550b7..6af66d0f86e 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -20,7 +20,6 @@ export default () => helpPagePath: environmentsData.helpPagePath, cssContainerClass: environmentsData.cssClass, canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment), - canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), }; }, @@ -32,7 +31,6 @@ export default () => helpPagePath: this.helpPagePath, cssContainerClass: this.cssContainerClass, canCreateEnvironment: this.canCreateEnvironment, - canCreateDeployment: this.canCreateDeployment, canReadEnvironment: this.canReadEnvironment, }, }); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 4d2533d01f1..9336b71cfd7 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -44,6 +44,7 @@ export default class IssuableForm { parse: dateString => parsePikadayDate(dateString), toString: date => pikadayToString(date), onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)), + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index e664269b199..58f14bac8c8 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; -import { __ } from '~/locale'; +import { s__, sprintf } from '~/locale'; +import createFlash from '~/flash'; import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; @@ -91,9 +92,14 @@ export default { }, taskListUpdateError() { - window.Flash( - __( - 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.', + createFlash( + sprintf( + s__( + 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.', + ), + { + issueType: this.issuableType, + }, ), ); diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js index b41ffb44971..82ee83e4348 100644 --- a/app/assets/javascripts/lib/utils/file_upload.js +++ b/app/assets/javascripts/lib/utils/file_upload.js @@ -1,6 +1,9 @@ export default (buttonSelector, fileSelector) => { const btn = document.querySelector(buttonSelector); const fileInput = document.querySelector(fileSelector); + + if (!btn || !fileInput) return; + const form = btn.closest('form'); btn.addEventListener('click', () => { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 8e10b3ad912..63db4938cd7 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -124,9 +124,6 @@ function deferredInitialisation() { selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', boundary: 'viewport', - placement(tip, el) { - return $(el).data('placement') || 'bottom'; - }, }); // Initialize popovers diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 0beedcacf33..0dabb28ea66 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d toggleClearInput.call($input); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($input.val())); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index ac3b47cd218..3b42a154af8 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { __ } from '~/locale'; +import createFlash from '~/flash'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; @@ -40,6 +41,13 @@ function MergeRequest(opts) { document.querySelector('#task_status').innerText = result.task_status; document.querySelector('#task_status_short').innerText = result.task_status_short; }, + onError: () => { + createFlash( + __( + 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.', + ), + ); + }, }); } } diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index ec0e33a1927..14c02db7bcc 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -189,8 +189,8 @@ export default { <template> <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> - <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> - <div class="prometheus-graph-widgets"><slot></slot></div> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart ref="areaChart" diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 9c5fd93f7d1..895a57785bc 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -160,7 +160,8 @@ export default { {{ s__('Metrics|Environment') }} <div class="dropdown prepend-left-10"> <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" /> + <span>{{ currentEnvironmentName }}</span> + <icon name="chevron-down" /> </button> <div v-if="store.environmentsData.length > 0" @@ -172,9 +173,8 @@ export default { :href="environment.metrics_path" :class="{ 'is-active': environment.name == currentEnvironmentName }" class="dropdown-item" + >{{ environment.name }}</a > - {{ environment.name }} - </a> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 394f2a80a67..cad0d382fa2 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -151,10 +151,9 @@ export default { </div> <div v-if="canAwardEmoji" class="note-actions-item"> <a - v-gl-tooltip.bottom + v-gl-tooltip :class="{ 'js-user-authored': isAuthoredByCurrentUser }" class="note-action-button note-emoji-button js-add-award js-note-emoji" - data-position="right" href="#" title="Add reaction" > @@ -175,7 +174,7 @@ export default { /> <div v-if="canEdit" class="note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="Edit comment" class="note-action-button js-note-edit btn btn-transparent" @@ -186,7 +185,7 @@ export default { </div> <div v-if="showDeleteAction" class="note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="Delete comment" class="note-action-button js-note-delete btn btn-transparent" @@ -197,7 +196,7 @@ export default { </div> <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item"> <button - v-gl-tooltip.bottom + v-gl-tooltip type="button" title="More actions" class="note-action-button more-actions-toggle btn btn-transparent" diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 3efdd1c5c17..17e5fcab5b7 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -171,7 +171,6 @@ export default { :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" data-boundary="viewport" - data-placement="bottom" class="btn award-control" type="button" @click="handleAward(awardName)" @@ -187,7 +186,6 @@ export default { title="Add reaction" aria-label="Add reaction" data-boundary="viewport" - data-placement="bottom" type="button" > <span class="award-control-icon award-control-icon-neutral"> diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index 0b644780ad4..0d69a689316 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ -import monitoringBundle from '~/monitoring/monitoring_bundle'; +import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle'; document.addEventListener('DOMContentLoaded', monitoringBundle); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 3ccad513c05..26d7fa7371d 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -43,10 +43,26 @@ document.addEventListener('DOMContentLoaded', () => { ], }); + const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { + if (firstDayOfWeek === 0) { + return weekDays; + } + + return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => { + const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length]; + + return { + ...acc, + [reorderedDayName]: weekDays[reorderedDayName], + }; + }, {}); + }; + const hourData = chartData(projectChartData.hour); responsiveChart($('#hour-chart'), hourData); - const dayData = chartData(projectChartData.weekDays); + const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); + const dayData = chartData(weekDays); responsiveChart($('#weekday-chart'), dayData); const monthData = chartData(projectChartData.month); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 8a84ac37dab..afa099d0e0b 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -159,7 +159,7 @@ export default class ActivityCalendar { .append('g') .attr('transform', (group, i) => { _.each(group, (stamp, a) => { - if (a === 0 && stamp.day === 0) { + if (a === 0 && stamp.day === this.firstDayOfWeek) { const month = stamp.date.getMonth(); const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace; const lastMonth = _.last(this.months); @@ -205,6 +205,14 @@ export default class ActivityCalendar { y: 29 + this.dayYPos(5), }, ]; + + if (this.firstDayOfWeek === 1) { + days.push({ + text: 'S', + y: 29 + this.dayYPos(7), + }); + } + this.svg .append('g') .selectAll('text') diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1c3fd58ca74..39cd891c111 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -234,7 +234,7 @@ export default class UserTabs { data, calendarActivitiesPath, utcOffset, - 0, + gon.first_day_of_week, monthsAgo, ); } diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 998554d1be5..d65e73a3f9c 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -115,15 +115,35 @@ const bindEvents = () => { const templates = { rails: { text: 'Ruby on Rails', - icon: '.template-option svg.icon-rails', + icon: '.template-option .icon-rails', }, express: { text: 'NodeJS Express', - icon: '.template-option svg.icon-node-express', + icon: '.template-option .icon-express', }, spring: { text: 'Spring', - icon: '.template-option svg.icon-java-spring', + icon: '.template-option .icon-spring', + }, + hugo: { + text: 'Pages/Hugo', + icon: '.template-option .icon-hugo', + }, + jekyll: { + text: 'Pages/Jekyll', + icon: '.template-option .icon-jekyll', + }, + plainhtml: { + text: 'Pages/Plain HTML', + icon: '.template-option .icon-plainhtml', + }, + gitbook: { + text: 'Pages/GitBook', + icon: '.template-option .icon-gitbook', + }, + hexo: { + text: 'Pages/Hexo', + icon: '.template-option .icon-hexo', }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue new file mode 100644 index 00000000000..a38f25cce35 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue @@ -0,0 +1,40 @@ +<script> +export default { + props: { + value: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + inputId: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <li> + <div class="commit-message-editor"> + <div class="d-flex flex-wrap align-items-center justify-content-between"> + <label class="col-form-label" :for="inputId"> + <strong>{{ label }}</strong> + </label> + <slot name="header"></slot> + </div> + <textarea + :id="inputId" + :value="value" + class="form-control js-gfm-input append-bottom-default commit-message-edit" + required="required" + rows="7" + @input="$emit('input', $event.target.value)" + ></textarea> + <slot name="checkbox"></slot> + </div> + </li> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue new file mode 100644 index 00000000000..b3c1c0e329d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -0,0 +1,38 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + props: { + commits: { + type: Array, + required: true, + default: () => [], + }, + }, +}; +</script> + +<template> + <div> + <gl-dropdown + right + no-caret + text="Use an existing commit message" + variant="link" + class="mr-commit-dropdown" + > + <gl-dropdown-item + v-for="commit in commits" + :key="commit.short_id" + class="text-nowrap text-truncate" + @click="$emit('input', commit.message)" + > + <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue new file mode 100644 index 00000000000..a1d3a09cca4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -0,0 +1,91 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import _ from 'underscore'; +import { __, n__, sprintf, s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + GlButton, + }, + props: { + isSquashEnabled: { + type: Boolean, + required: true, + }, + commitsCount: { + type: Number, + required: false, + default: 0, + }, + targetBranch: { + type: String, + required: true, + }, + }, + data() { + return { + expanded: false, + }; + }, + computed: { + collapseIcon() { + return this.expanded ? 'chevron-down' : 'chevron-right'; + }, + commitsCountMessage() { + return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount); + }, + modifyLinkMessage() { + return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit'); + }, + ariaLabel() { + return this.expanded ? __('Collapse') : __('Expand'); + }, + message() { + return sprintf( + s__( + 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.', + ), + { + commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`, + mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`, + targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`, + }, + false, + ); + }, + }, + methods: { + toggle() { + this.expanded = !this.expanded; + }, + }, +}; +</script> + +<template> + <div> + <div + class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2" + @click="toggle()" + > + <gl-button + :aria-label="ariaLabel" + variant="blank" + class="commit-edit-toggle mr-2" + @click.stop="toggle()" + > + <icon :name="collapseIcon" :size="16" /> + </gl-button> + <span v-if="expanded">{{ __('Collapse') }}</span> + <span v-else> + <span v-html="message"></span> + <gl-button variant="link" class="modify-message-button"> + {{ modifyLinkMessage }} + </gl-button> + </span> + </div> + <div v-show="expanded"><slot></slot></div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index b8f29649eb5..ce4207864ea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -2,17 +2,24 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import { __ } from '~/locale'; import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; import SquashBeforeMerge from './squash_before_merge.vue'; +import CommitsHeader from './commits_header.vue'; +import CommitEdit from './commit_edit.vue'; +import CommitMessageDropdown from './commit_message_dropdown.vue'; export default { name: 'ReadyToMerge', components: { statusIcon, SquashBeforeMerge, + CommitsHeader, + CommitEdit, + CommitMessageDropdown, }, props: { mr: { type: Object, required: true }, @@ -22,27 +29,20 @@ export default { return { removeSourceBranch: this.mr.shouldRemoveSourceBranch, mergeWhenBuildSucceeds: false, - useCommitMessageWithDescription: false, setToMergeWhenPipelineSucceeds: false, - showCommitMessageEditor: false, isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, squashBeforeMerge: this.mr.squash, successSvg, warningSvg, + squashCommitMessage: this.mr.squashCommitMessage, }; }, computed: { shouldShowMergeWhenPipelineSucceedsText() { return this.mr.isPipelineActive; }, - commitMessageLinkTitle() { - const withDesc = 'Include description in commit message'; - const withoutDesc = "Don't include description in commit message"; - - return this.useCommitMessageWithDescription ? withoutDesc : withDesc; - }, status() { const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -84,9 +84,9 @@ export default { }, mergeButtonText() { if (this.isMergingImmediately) { - return 'Merge in progress'; + return __('Merge in progress'); } else if (this.shouldShowMergeWhenPipelineSucceedsText) { - return 'Merge when pipeline succeeds'; + return __('Merge when pipeline succeeds'); } return 'Merge'; @@ -98,7 +98,7 @@ export default { const { commitMessage } = this; return Boolean( !commitMessage.length || - !this.shouldShowMergeControls() || + !this.shouldShowMergeControls || this.isMakingRequest || this.mr.preventMerge, ); @@ -110,18 +110,14 @@ export default { const { commitsCount, enableSquashBeforeMerge } = this.mr; return enableSquashBeforeMerge && commitsCount > 1; }, - }, - methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; }, - updateCommitMessage() { - const cmwd = this.mr.commitMessageWithDescription; - this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription; - this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage; - }, - toggleCommitMessageEditor() { - this.showCommitMessageEditor = !this.showCommitMessageEditor; + }, + methods: { + updateMergeCommitMessage(includeDescription) { + const { commitMessageWithDescription, commitMessage } = this.mr; + this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; }, handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) { // TODO: Remove no-param-reassign @@ -139,6 +135,7 @@ export default { merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, squash: this.squashBeforeMerge, + squash_commit_message: this.squashCommitMessage, }; this.isMakingRequest = true; @@ -158,7 +155,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line }); }, initiateMergePolling() { @@ -194,7 +191,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line }); }, initiateRemoveSourceBranchPolling() { @@ -223,7 +220,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line }); }, }, @@ -231,127 +228,136 @@ export default { </script> <template> - <div class="mr-widget-body media"> - <status-icon :status="iconClass" /> - <div class="media-body"> - <div class="mr-widget-body-controls media space-children"> - <span class="btn-group"> - <button - :disabled="isMergeButtonDisabled" - :class="mergeButtonClass" - type="button" - class="qa-merge-button" - @click="handleMergeButtonClick()" - > - <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> - {{ mergeButtonText }} - </button> - <button - v-if="shouldShowMergeOptionsDropdown" - :disabled="isMergeButtonDisabled" - type="button" - class="btn btn-sm btn-info dropdown-toggle js-merge-moment" - data-toggle="dropdown" - aria-label="Select merge moment" - > - <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> - </button> - <ul - v-if="shouldShowMergeOptionsDropdown" - class="dropdown-menu dropdown-menu-right" - role="menu" - > - <li> - <a - class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" - href="#" - @click.prevent="handleMergeButtonClick(true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> - <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> - </span> - </a> - </li> - <li> - <a - class="accept-merge-request qa-merge-immediately-option" - href="#" - @click.prevent="handleMergeButtonClick(false, true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> - <span class="media-body merge-opt-title">Merge immediately</span> - </span> - </a> - </li> - </ul> - </span> - <div class="media-body-wrap space-children"> - <template v-if="shouldShowMergeControls()"> - <label v-if="mr.canRemoveSourceBranch"> - <input - id="remove-source-branch-input" - v-model="removeSourceBranch" - :disabled="isRemoveSourceBranchButtonDisabled" - class="js-remove-source-branch-checkbox" - type="checkbox" - /> - Delete source branch - </label> - - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - v-model="squashBeforeMerge" - :help-path="mr.squashBeforeMergeHelpPath" - :is-disabled="isMergeButtonDisabled" - /> - - <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> - Fast-forward merge without a merge commit - </span> + <div> + <div class="mr-widget-body media"> + <status-icon :status="iconClass" /> + <div class="media-body"> + <div class="mr-widget-body-controls media space-children"> + <span class="btn-group"> <button - v-else :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-sm" + :class="mergeButtonClass" type="button" - @click="toggleCommitMessageEditor" + class="qa-merge-button" + @click="handleMergeButtonClick()" > - Modify commit message + <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + {{ mergeButtonText }} </button> - </template> - <template v-else> - <span class="bold js-resolve-mr-widget-items-message"> - You can only merge once the items above are resolved - </span> - </template> - </div> - </div> - <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor"> - <div class="form-group clearfix"> - <label class="col-form-label" for="commit-message"> Commit message </label> - <div class="col-sm-10"> - <div class="commit-message-container"> - <div class="max-width-marker"></div> - <textarea - id="commit-message" - v-model="commitMessage" - class="form-control js-commit-message" - required="required" - rows="14" - name="Commit message" - ></textarea> - </div> - <p class="hint"> - Try to keep the first line under 52 characters and the others under 72 - </p> - <div class="hint"> - <a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a> - </div> + <button + v-if="shouldShowMergeOptionsDropdown" + :disabled="isMergeButtonDisabled" + type="button" + class="btn btn-sm btn-info dropdown-toggle js-merge-moment" + data-toggle="dropdown" + aria-label="Select merge moment" + > + <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> + </button> + <ul + v-if="shouldShowMergeOptionsDropdown" + class="dropdown-menu dropdown-menu-right" + role="menu" + > + <li> + <a + class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" + href="#" + @click.prevent="handleMergeButtonClick(true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> + <span class="media-body merge-opt-title">{{ + __('Merge when pipeline succeeds') + }}</span> + </span> + </a> + </li> + <li> + <a + class="accept-merge-request qa-merge-immediately-option" + href="#" + @click.prevent="handleMergeButtonClick(false, true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> + <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span> + </span> + </a> + </li> + </ul> + </span> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls"> + <label v-if="mr.canRemoveSourceBranch"> + <input + id="remove-source-branch-input" + v-model="removeSourceBranch" + :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" + type="checkbox" + /> + {{ __('Delete source branch') }} + </label> + + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" + /> + </template> + <template v-else> + <span class="bold js-resolve-mr-widget-items-message"> + {{ __('You can only merge once the items above are resolved') }} + </span> + </template> </div> </div> </div> </div> + <template v-if="shouldShowMergeControls"> + <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message"> + {{ __('Fast-forward merge without a merge commit') }} + </div> + <template v-else> + <commits-header + :is-squash-enabled="squashBeforeMerge" + :commits-count="mr.commitsCount" + :target-branch="mr.targetBranch" + > + <ul class="border-top content-list commits-list flex-list"> + <commit-edit + v-if="squashBeforeMerge" + v-model="squashCommitMessage" + :label="__('Squash commit message')" + input-id="squash-message-edit" + squash + > + <commit-message-dropdown + slot="header" + v-model="squashCommitMessage" + :commits="mr.commits" + /> + </commit-edit> + <commit-edit + v-model="commitMessage" + :label="__('Merge commit message')" + input-id="merge-message-edit" + > + <label slot="checkbox"> + <input + id="include-description" + type="checkbox" + @change="updateMergeCommitMessage($event.target.checked)" + /> + {{ __('Include merge request description') }} + </label> + </commit-edit> + </ul> + </commits-header> + </template> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 57c4dfbe3b7..abbbe19c5ef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -315,7 +315,7 @@ export default { :endpoint="mr.testResultsPath" /> - <div class="mr-widget-section"> + <div class="mr-widget-section p-0"> <component :is="componentName" :mr="mr" :service="service" /> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 36cac230d9d..58363f632a9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -42,6 +42,8 @@ export default class MergeRequestStore { this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; this.postMergeDeployments = this.postMergeDeployments || []; + this.commits = data.commits_without_merge_commits || []; + this.squashCommitMessage = data.default_squash_commit_message; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 8bdb5bf22c2..13eb46437dd 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -40,6 +40,7 @@ export default { toString: date => pikadayToString(date), onSelect: this.selected.bind(this), onClose: this.toggled.bind(this), + firstDay: gon.first_day_of_week, }); this.$el.append(this.calendar.el); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cb449b642e7..c5c3b66438c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -391,6 +391,11 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .overflow-auto { overflow: auto; } +.d-flex-center { + display: flex; + align-items: center; + justify-content: center; +} /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } @@ -402,6 +407,10 @@ img.emoji { .min-height-0 { min-height: 0; } +.w-3 { width: #{3 * $grid-size}; } + +.h-3 { width: #{3 * $grid-size}; } + /** COMMON SPACING CLASSES **/ .gl-pl-0 { padding-left: 0; } .gl-pl-1 { padding-left: #{0.5 * $grid-size}; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 037a5adfb7e..6108eaa1ad0 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -4,6 +4,7 @@ */ .file-holder { border: 1px solid $border-color; + border-top: 0; border-radius: $border-radius-default; &.file-holder-no-border { @@ -51,6 +52,7 @@ position: absolute; top: 5px; right: 15px; + margin-left: auto; .btn { padding: 0 10px; @@ -324,10 +326,12 @@ span.idiff { &, .file-holder & { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; background-color: $gray-light; border-bottom: 1px solid $border-color; + border-top: 1px solid $border-color; padding: 5px $gl-padding; margin: 0; border-radius: $border-radius-default $border-radius-default 0 0; @@ -365,16 +369,12 @@ span.idiff { margin: 0 10px 0 0; } - .file-actions { - white-space: nowrap; - - .btn { - padding: 0 10px; - font-size: 13px; - line-height: 28px; - display: inline-block; - float: none; - } + .file-actions .btn { + padding: 0 10px; + font-size: 13px; + line-height: 28px; + display: inline-block; + float: none; } @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9eae9a831fa..96dab609a13 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -243,6 +243,7 @@ $gl-padding-8: 8px; $gl-padding: 16px; $gl-padding-24: 24px; $gl-padding-32: 32px; +$gl-padding-50: 50px; $gl-col-padding: 15px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -490,6 +491,7 @@ $builds-trace-bg: #111; */ $commit-max-width-marker-color: rgba(0, 0, 0, 0); $commit-message-text-area-bg: rgba(0, 0, 0, 0); +$commit-stat-summary-height: 36px; /* * Common @@ -664,8 +666,14 @@ $priority-label-empty-state-width: 114px; Issues Analytics */ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15); + /* Merge Requests */ $mr-tabs-height: 51px; $mr-version-controls-height: 56px; + +/* +Compare Branches +*/ +$compare-branches-sticky-header-height: 68px; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index ad12cd101b6..809ba6d4953 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -58,6 +58,20 @@ } } +.cluster-application-banner { + height: 45px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.cluster-application-banner-close { + align-self: flex-start; + font-weight: 500; + font-size: 20px; + margin: $gl-padding-8 14px 0 0; +} + .cluster-application-description { flex: 1; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 02aac58a475..e3b98b26a11 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -7,22 +7,13 @@ cursor: pointer; @media (min-width: map-get($grid-breakpoints, md)) { + $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height; + position: -webkit-sticky; position: sticky; - top: $mr-version-controls-height + $header-height + $mr-tabs-height; - margin-left: -1px; - border-left: 1px solid $border-color; + top: $mr-file-header-top; z-index: 102; - &.is-commit { - top: $header-height + 36px; - - .with-performance-bar & { - top: $header-height + 36px + $performance-bar-height; - - } - } - &::before { content: ''; position: absolute; @@ -35,7 +26,23 @@ } .with-performance-bar & { - top: $header-height + $performance-bar-height + $mr-version-controls-height + $mr-tabs-height; + top: $mr-file-header-top + $performance-bar-height; + } + + &.is-commit { + top: $header-height + $commit-stat-summary-height; + + .with-performance-bar & { + top: $header-height + $commit-stat-summary-height + $performance-bar-height; + } + } + + &.is-compare { + top: $header-height + $compare-branches-sticky-header-height; + + .with-performance-bar & { + top: $performance-bar-height + $header-height + $compare-branches-sticky-header-height; + } } } @@ -501,6 +508,25 @@ } } +.diff-stats { + align-items: center; + padding: 0 .25rem; + + .diff-stats-group { + padding: 0 .25rem; + } + + svg.diff-stats-icon { + vertical-align: text-bottom; + } + + &.is-compare-versions-header { + .diff-stats-group { + padding: 0 .5rem; + } + } +} + .file-content .diff-file { margin: 0; border: 0; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 38a7e199c6a..135730d71e9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -38,9 +38,7 @@ } .mr-widget-section { - .media { - align-items: center; - } + border-radius: $border-radius-default $border-radius-default 0 0; .code-text { flex: 1; @@ -56,6 +54,11 @@ .mr-widget-extension { border-top: 1px solid $border-color; background-color: $gray-light; + + &.clickable:hover { + background-color: $gl-gray-200; + cursor: pointer; + } } .mr-widget-workflow { @@ -78,6 +81,7 @@ border-top: 0; } +.mr-widget-body, .mr-widget-section, .mr-widget-content, .mr-widget-footer { @@ -87,11 +91,38 @@ .mr-state-widget { color: $gl-text-color; + .commit-message-edit { + border-radius: $border-radius-default; + } + .mr-widget-section, .mr-widget-footer { border-top: solid 1px $border-color; } + .mr-fast-forward-message { + padding-left: $gl-padding-50; + padding-bottom: $gl-padding; + } + + .commits-list { + > li { + padding: $gl-padding; + + @include media-breakpoint-up(md) { + padding-left: $gl-padding-50; + } + } + } + + .mr-commit-dropdown { + .dropdown-menu { + @include media-breakpoint-up(md) { + width: 150%; + } + } + } + .mr-widget-footer { padding: 0; } @@ -405,7 +436,7 @@ } .mr-widget-help { - padding: 10px 16px 10px 48px; + padding: 10px 16px 10px $gl-padding-50; font-style: italic; } @@ -423,10 +454,6 @@ } } -.mr-widget-body-controls { - flex-wrap: wrap; -} - .mr_source_commit, .mr_target_commit { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2342c284a5e..66866aedfba 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -507,12 +507,6 @@ } .template-option { - .logo { - .btn-template-icon { - width: 40px !important; - } - } - padding: 16px 0; &:not(:first-child) { @@ -551,9 +545,8 @@ } .selected-icon { - svg { + img { display: none; - top: 7px; height: 20px; width: 20px; } @@ -946,6 +939,11 @@ pre.light-well { .flex-wrapper { min-width: 0; margin-top: -$gl-padding-8; // negative margin required for flex-wrap + flex: 1 1 100%; + + .project-title { + line-height: 20px; + } } p, @@ -984,14 +982,16 @@ pre.light-well { } .controls { - margin-top: $gl-padding-8; + @include media-breakpoint-down(xs) { + margin-top: $gl-padding-8; + } - @include media-breakpoint-down(md) { + @include media-breakpoint-up(sm) { margin-top: 0; } - @include media-breakpoint-down(xs) { - margin-top: $gl-padding-8; + @include media-breakpoint-up(lg) { + flex: 1 1 40%; } .icon-wrapper { @@ -1041,7 +1041,7 @@ pre.light-well { min-height: 40px; min-width: 40px; - .identicon.s64 { + .identicon.s48 { font-size: 16px; } } diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index b9717b97640..3bd91b71d92 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -127,6 +127,7 @@ class Clusters::ClustersController < Clusters::BaseController params.require(:cluster).permit( :enabled, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :namespace ] @@ -136,6 +137,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :api_url, :token, diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 97120273d6b..cc2bb99f55b 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -116,8 +116,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController session[:service_tickets][provider] = ticket end + def build_auth_user(auth_user_class) + auth_user_class.new(oauth) + end + def sign_in_user_flow(auth_user_class) - auth_user = auth_user_class.new(oauth) + auth_user = build_auth_user(auth_user_class) user = auth_user.find_and_update! if auth_user.valid_sign_in? diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index a27e3cceaeb..94002095739 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -37,6 +37,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController end def preferences_param_names - [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id] + [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id, :first_day_of_week] end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 79685e8b675..e9cd475a199 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -11,11 +11,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index] - before_action do - push_frontend_feature_flag(:area_chart, project) - end - - # Returns all environments or all folders based on the :nested param def index @environments = project.environments .with_state(params[:scope] || :available) diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 9e403e1d25b..88d0755f41f 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -15,6 +15,14 @@ class Projects::ErrorTrackingController < Projects::ApplicationController end end + def list_projects + respond_to do |format| + format.json do + render_project_list_json + end + end + end + private def render_index_json @@ -32,6 +40,32 @@ class Projects::ErrorTrackingController < Projects::ApplicationController } end + def render_project_list_json + service = ErrorTracking::ListProjectsService.new( + project, + current_user, + list_projects_params + ) + result = service.execute + + if result[:status] == :success + render json: { + projects: serialize_projects(result[:projects]) + } + else + return render( + status: result[:http_status] || :bad_request, + json: { + message: result[:message] + } + ) + end + end + + def list_projects_params + params.require(:error_tracking_setting).permit([:api_host, :token]) + end + def set_polling_interval Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) end @@ -41,4 +75,10 @@ class Projects::ErrorTrackingController < Projects::ApplicationController .new(project: project, user: current_user) .represent(errors) end + + def serialize_projects(projects) + ErrorTracking::ProjectSerializer + .new(project: project, user: current_user) + .represent(projects) + end end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 54ff7ded8e5..6045ee4e171 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -34,7 +34,8 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :task_num, :title, :discussion_locked, - label_ids: [] + label_ids: [], + update_task: [:index, :checked, :line_number, :line_source] ] end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3e08c0ccd8f..23af2e0521c 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -305,7 +305,7 @@ class IssuableFinder def use_subquery_for_search? strong_memoize(:use_subquery_for_search) do attempt_group_search_optimizations? && - Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false) + Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: true) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index c8e4e2e3df9..e635f608237 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -150,6 +150,7 @@ module ApplicationSettingsHelper :email_author_in_body, :enabled_git_access_protocol, :enforce_terms, + :first_day_of_week, :gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast, @@ -231,7 +232,8 @@ module ApplicationSettingsHelper :web_ide_clientside_preview_enabled, :diff_max_patch_bytes, :commit_email_hostname, - :protected_ci_variables + :protected_ci_variables, + :local_markdown_version ] end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 516c8a353ea..67e7e475920 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -9,41 +9,4 @@ module AutoDevopsHelper !project.repository.gitlab_ci_yml && !project.ci_service end - - def auto_devops_warning_message(project) - if missing_auto_devops_service?(project) - params = { - kubernetes: link_to('Kubernetes cluster', project_clusters_path(project)) - } - - if missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params - else - _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params - end - elsif missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def cluster_ingress_ip(project) - project - .cluster_ingresses - .where("external_ip is not null") - .limit(1) - .pluck(:external_ip) - .first - end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def missing_auto_devops_domain?(project) - !(project.auto_devops || project.build_auto_devops)&.has_domain? - end - - def missing_auto_devops_service?(project) - !project.deployment_platform&.active? - end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index b3935ae350d..365b94f5a3e 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -11,7 +11,6 @@ module EnvironmentsHelper { "endpoint" => folder_project_environments_path(@project, @folder, format: :json), "folder-name" => @folder, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s } end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index f4f46b0fe96..bc1742e8167 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -43,6 +43,17 @@ module PreferencesHelper ] end + def first_day_of_week_choices + [ + [_('Sunday'), 0], + [_('Monday'), 1] + ] + end + + def first_day_of_week_choices_with_default + first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil]) + end + def user_application_theme @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class end @@ -66,4 +77,8 @@ module PreferencesHelper def excluded_dashboard_choices ['operations'] end + + def default_first_day_of_week + first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 88746375c67..daadf9427ba 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -193,6 +193,10 @@ class ApplicationSetting < ActiveRecord::Base allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + validates :local_markdown_version, + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -246,6 +250,7 @@ class ApplicationSetting < ActiveRecord::Base dsa_key_restriction: 0, ecdsa_key_restriction: 0, ed25519_key_restriction: 0, + first_day_of_week: 0, gitaly_timeout_default: 55, gitaly_timeout_fast: 10, gitaly_timeout_medium: 30, @@ -303,7 +308,8 @@ class ApplicationSetting < ActiveRecord::Base usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, - protected_ci_variables: false + protected_ci_variables: false, + local_markdown_version: 0 } end diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 26bf73f4dd8..52c440ffb2f 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -53,11 +53,11 @@ module Clusters end def upgrade_command(values) - ::Gitlab::Kubernetes::Helm::UpgradeCommand.new( - name, + ::Gitlab::Kubernetes::Helm::InstallCommand.new( + name: name, version: VERSION, - chart: chart, rbac: cluster.platform_kubernetes_rbac?, + chart: chart, files: files_with_replaced_values(values) ) end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index a2c48973fa5..7025fc2cc02 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -18,6 +18,7 @@ module Clusters Applications::Knative.application_name => Applications::Knative }.freeze DEFAULT_ENVIRONMENT = '*'.freeze + KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze belongs_to :user @@ -49,7 +50,7 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? @@ -65,6 +66,9 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true delegate :available?, to: :application_knative, prefix: true, allow_nil: true + delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true + + alias_attribute :base_domain, :domain enum cluster_type: { instance_type: 1, @@ -193,8 +197,42 @@ module Clusters project_type? end + def kube_ingress_domain + @kube_ingress_domain ||= domain.presence || instance_domain || legacy_auto_devops_domain + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + break variables unless kube_ingress_domain + + variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain) + end + end + private + def instance_domain + @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain + end + + # To keep backward compatibility with AUTO_DEVOPS_DOMAIN + # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN + # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options: + # ProjectAutoDevops#Domain, project variables or group variables, + # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL + # + # This method should is scheduled to be removed on + # https://gitlab.com/gitlab-org/gitlab-ce/issues/56959 + def legacy_auto_devops_domain + if project_type? + project&.auto_devops&.domain.presence || + project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence || + project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + elsif group_type? + group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + end + end + def restrict_modification if provider&.on_creation? errors.add(:base, "cannot modify during creation") diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index a556dd5ad8b..6fe7b4a6bd7 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -20,7 +20,7 @@ module Clusters state :update_errored, value: 6 event :make_scheduled do - transition [:installable, :errored] => :scheduled + transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled end event :make_installing do @@ -29,16 +29,19 @@ module Clusters event :make_installed do transition [:installing] => :installed + transition [:updating] => :updated end event :make_errored do - transition any => :errored + transition any - [:updating] => :errored + transition [:updating] => :update_errored end event :make_updating do - transition [:installed, :updated, :update_errored] => :updating + transition [:installed, :updated, :update_errored, :scheduled] => :updating end + # Deprecated event :make_updated do transition [:updating] => :updated end @@ -74,6 +77,10 @@ module Clusters end end + def updateable? + installed? || updated? || update_errored? + end + def available? installed? || updated? end diff --git a/app/models/clusters/concerns/application_version.rb b/app/models/clusters/concerns/application_version.rb index e355de23df6..db94e8e08c9 100644 --- a/app/models/clusters/concerns/application_version.rb +++ b/app/models/clusters/concerns/application_version.rb @@ -12,6 +12,10 @@ module Clusters end end end + + def update_available? + version != self.class.const_get(:VERSION) + end end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 8f3424db295..c8969351ed9 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -98,6 +98,8 @@ module Clusters .append(key: 'KUBE_NAMESPACE', value: actual_namespace) .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) end + + variables.concat(cluster.predefined_variables) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 5fa6f79bdaa..1a8570b80c3 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -115,7 +115,28 @@ module CacheMarkdownField end def latest_cached_markdown_version - CacheMarkdownField::CACHE_COMMONMARK_VERSION + @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version + end + + def local_version + # because local_markdown_version is stored in application_settings which + # uses cached_markdown_version too, we check explicitly to avoid + # endless loop + return local_markdown_version if has_attribute?(:local_markdown_version) + + settings = Gitlab::CurrentSettings.current_application_settings + + # Following migrations are not properly isolated and + # use real models (by calling .ghost method), in these migrations + # local_markdown_version attribute doesn't exist yet, so we + # use a default value: + # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb + # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb + if settings.respond_to?(:local_markdown_version) + settings.local_markdown_version + else + 0 + end end included do diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 34220c1b450..4635fc72dc7 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -96,7 +96,9 @@ class PoolRepository < ActiveRecord::Base @object_pool ||= Gitlab::Git::ObjectPool.new( shard.name, disk_path + '.git', - source_project.repository.raw) + source_project.repository.raw, + source_project.full_path + ) end def inspect diff --git a/app/models/project.rb b/app/models/project.rb index d4e2ed883bc..8f746f6e094 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1288,7 +1288,7 @@ class Project < ActiveRecord::Base # Forked import is handled asynchronously return if forked? && !force - if gitlab_shell.create_repository(repository_storage, disk_path) + if gitlab_shell.create_project_repository(self) repository.after_create true else diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 2253ad7b543..e353a6443c4 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -24,6 +24,12 @@ class ProjectAutoDevops < ActiveRecord::Base domain.present? || instance_domain.present? end + # From 11.8, AUTO_DEVOPS_DOMAIN has been replaced by KUBE_INGRESS_BASE_DOMAIN. + # See Clusters::Cluster#predefined_variables and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580 + # for more info. + # + # Suppport AUTO_DEVOPS_DOMAIN is scheduled to be removed on + # https://gitlab.com/gitlab-org/gitlab-ce/issues/52363 def predefined_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| if has_domain? diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 559e4f99294..c43bd45a62f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -60,7 +60,7 @@ class ProjectWiki def wiki @wiki ||= begin gl_repository = Gitlab::GlRepository.gl_repository(project, true) - raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository) + raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path) create_repo!(raw_repository) unless raw_repository.exists? @@ -175,7 +175,7 @@ class ProjectWiki private def create_repo!(raw_repository) - gitlab_shell.create_repository(project.repository_storage, disk_path) + gitlab_shell.create_wiki_repository(project) raise CouldNotCreateWikiError unless raw_repository.exists? diff --git a/app/models/repository.rb b/app/models/repository.rb index bfd2608bed4..7c50b4488e5 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1104,6 +1104,9 @@ class Repository end def initialize_raw_repository - Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki)) + Gitlab::Git::Repository.new(project.repository_storage, + disk_path + '.git', + Gitlab::GlRepository.gl_repository(project, is_wiki), + project.full_path) end end diff --git a/app/models/user.rb b/app/models/user.rb index 9c091ac366c..24101eda0b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -228,6 +228,9 @@ class User < ApplicationRecord delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :notes_filter_for, to: :user_preference delegate :set_notes_filter, to: :user_preference + delegate :first_day_of_week, :first_day_of_week=, to: :user_preference + + accepts_nested_attributes_for :user_preference, update_only: true state_machine :state, initial: :active do event :block do diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index 62b23a889c8..02df1480828 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -8,4 +8,5 @@ class ClusterApplicationEntity < Grape::Entity expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } + expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) } end diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index aa1d9e6292c..34ae06278c8 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -24,6 +24,12 @@ class DeploymentEntity < Grape::Entity expose :user, using: UserEntity expose :commit, using: CommitEntity expose :deployable, using: JobEntity - expose :manual_actions, using: JobEntity - expose :scheduled_actions, using: JobEntity + expose :manual_actions, using: JobEntity, if: -> (*) { can_create_deployment? } + expose :scheduled_actions, using: JobEntity, if: -> (*) { can_create_deployment? } + + private + + def can_create_deployment? + can?(request.current_user, :create_deployment, request.project) + end end diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb index b2406f4d631..68724088fff 100644 --- a/app/serializers/error_tracking/project_serializer.rb +++ b/app/serializers/error_tracking/project_serializer.rb @@ -2,6 +2,6 @@ module ErrorTracking class ProjectSerializer < BaseSerializer - entity ProjectEntity + entity ErrorTracking::ProjectEntity end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index f764536e762..e95ba09c006 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -160,7 +160,8 @@ module Auth ## # We still support legacy pipeline triggers which do not have associated # actor. New permissions model and new triggers are always associated with - # an actor, so this should be improved in 10.0 version of GitLab. + # an actor. So this should be improved once + # https://gitlab.com/gitlab-org/gitlab-ce/issues/37452 is resolved. # def build_can_push?(requested_project) # Build can push only to the project from which it originates diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 21ec26ea233..c592d608b89 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -4,7 +4,7 @@ module Clusters module Applications class CheckInstallationProgressService < BaseHelmService def execute - return unless app.installing? + return unless operation_in_progress? case installation_phase when Gitlab::Kubernetes::Pod::SUCCEEDED @@ -16,11 +16,16 @@ module Clusters end rescue Kubeclient::HttpError => e log_error(e) - app.make_errored!("Kubernetes error: #{e.error_code}") unless app.errored? + + app.make_errored!("Kubernetes error: #{e.error_code}") end private + def operation_in_progress? + app.installing? || app.updating? + end + def on_success app.make_installed! ensure @@ -28,13 +33,13 @@ module Clusters end def on_failed - app.make_errored!("Installation failed. Check pod logs for #{install_command.pod_name} for more details.") + app.make_errored!("Operation failed. Check pod logs for #{pod_name} for more details.") end def check_timeout if timeouted? begin - app.make_errored!("Installation timed out. Check pod logs for #{install_command.pod_name} for more details.") + app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.") end else ClusterWaitForAppInstallationWorker.perform_in( @@ -42,20 +47,24 @@ module Clusters end end + def pod_name + install_command.pod_name + end + def timeouted? Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT end def remove_installation_pod - helm_api.delete_pod!(install_command.pod_name) + helm_api.delete_pod!(pod_name) end def installation_phase - helm_api.status(install_command.pod_name) + helm_api.status(pod_name) end def installation_errors - helm_api.log(install_command.pod_name) + helm_api.log(pod_name) end end end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb index d75ba70c27e..15c93f1e79b 100644 --- a/app/services/clusters/applications/schedule_installation_service.rb +++ b/app/services/clusters/applications/schedule_installation_service.rb @@ -10,6 +10,18 @@ module Clusters end def execute + application.updateable? ? schedule_upgrade : schedule_install + end + + private + + def schedule_upgrade + application.make_scheduled! + + ClusterUpgradeAppWorker.perform_async(application.name, application.id) + end + + def schedule_install application.make_scheduled! ClusterInstallAppWorker.perform_async(application.name, application.id) diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb new file mode 100644 index 00000000000..a0ece1d2635 --- /dev/null +++ b/app/services/clusters/applications/upgrade_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class UpgradeService < BaseHelmService + def execute + return unless app.scheduled? + + begin + app.make_updating! + + # install_command works with upgrades too + # as it basically does `helm upgrade --install` + helm_api.update(install_command) + + ClusterWaitForAppInstallationWorker.perform_in( + ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) + rescue Kubeclient::HttpError => e + log_error(e) + app.make_update_errored!("Kubernetes error: #{e.error_code}") + rescue StandardError => e + log_error(e) + app.make_update_errored!("Can't start upgrade process.") + end + end + end + end +end diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index e563447c64c..be33947d0eb 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -8,6 +8,7 @@ module Labels # returns the updated label def execute(label) + params[:name] = params.delete(:new_name) if params.key?(:new_name) params[:color] = convert_color_name_to_hex if params[:color].present? label.update(params) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 86a04587f79..8112c2a4299 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -21,7 +21,7 @@ module MergeRequests end handle_wip_event(merge_request) - update(merge_request) + update_task_event(merge_request) || update(merge_request) end # rubocop:disable Metrics/AbcSize @@ -83,6 +83,11 @@ module MergeRequests end # rubocop:enable Metrics/AbcSize + def handle_task_changes(merge_request) + todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.update_merge_request(merge_request, current_user) + end + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 5861b803996..7214e9efaf6 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -73,7 +73,7 @@ module Projects project.ensure_repository project.repository.fetch_as_mirror(project.import_url, refmap: refmap) else - gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url) + gitlab_shell.import_project_repository(project) end rescue Gitlab::Shell::Error => e # Expire cache to prevent scenarios such as: diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb index b5c4cd3235d..cfe187d9b12 100644 --- a/app/services/task_list_toggle_service.rb +++ b/app/services/task_list_toggle_service.rb @@ -32,7 +32,8 @@ class TaskListToggleService source_line_index = line_number - 1 markdown_task = source_lines[source_line_index] - return unless markdown_task == line_source + # The source in the DB could be using either \n or \r\n line endings + return unless markdown_task == line_source || markdown_task == line_source + "\r" return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task) currently_checked = TaskList::Item.new(source_checkbox[1]).complete? diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml new file mode 100644 index 00000000000..95d016a94a5 --- /dev/null +++ b/app/views/admin/application_settings/_localization.html.haml @@ -0,0 +1,11 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :first_day_of_week, _('Default first day of the week'), class: 'label-bold' + = f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control' + .form-text.text-muted + = _('Default first day of the week in calendars and date pickers.') + + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml index 00000b86ab7..c468d69d7b8 100644 --- a/app/views/admin/application_settings/preferences.html.haml +++ b/app/views/admin/application_settings/preferences.html.haml @@ -56,3 +56,14 @@ = _('Configure Gitaly timeouts.') .settings-content = render 'gitaly' + +%section.settings.as-localization.no-animate#js-localization-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Localization') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various localization settings.') + .settings-content + = render 'localization' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index a758a63dfb3..8d9c083d223 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -3,7 +3,7 @@ - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: [(award_state_class(awardable, awards, current_user))], - data: { placement: "bottom", title: award_user_list(awards, current_user) } } + data: { title: award_user_list(awards, current_user) } } = emoji_icon(emoji) %span.award-control-text.js-counter = awards.count @@ -12,7 +12,7 @@ .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': _('Add reaction'), - data: { title: _('Add reaction'), placement: "bottom" } } + data: { title: _('Add reaction') } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 4c47e11927e..7acd9ce0562 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -20,12 +20,27 @@ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") - else = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true - - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain') - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } .form-text.text-muted %code * = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } + .form-group + %h5= s_('ClusterIntegration|Base domain') + = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus' + .form-text.text-muted + - auto_devops_url = help_page_path('topics/autodevops/index') + - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } + = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe } + - if @cluster.application_ingress_external_ip.present? + = s_('ClusterIntegration|Alternatively') + %code #{@cluster.application_ingress_external_ip}.nip.io + = s_('ClusterIntegration| can be used instead of a custom domain.') + - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip') + - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url } + = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe } + - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index c1616810185..1a9aca1f6bf 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -60,5 +60,21 @@ = f.select :project_view, project_view_choices, {}, class: 'form-control' .form-text.text-muted Choose what content you want to see on a project’s overview page. + + .col-sm-12 + %hr + + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + = _('Localization') + %p + = _('Customize language and region related settings.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' + .col-lg-8 + .form-group + = f.label :first_day_of_week, class: 'label-bold' do + = _('First day of the week') + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' .form-group - = f.submit 'Save changes', class: 'btn btn-success' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 06f0cd9675e..fe9a8ac4182 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -10,7 +10,7 @@ .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" = render "ci_menu" - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-commit" .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index b6bebbabed0..5774b48a054 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-compare" - else .card.bg-light .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index cc2d0d3b2d8..2dba3fcd664 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,7 +2,7 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -- is_commit = local_assigns.fetch(:is_commit, false) +- diff_page_context = local_assigns.fetch(:diff_page_context, nil) .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .files-changed-inner @@ -25,4 +25,4 @@ = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, is_commit: is_commit } + = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 5565ae1d98b..855b719dc45 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,11 +1,11 @@ - environment = local_assigns.fetch(:environment, nil) -- is_commit = local_assigns.fetch(:is_commit, false) +- diff_page_context = local_assigns.fetch(:diff_page_context, nil) - file_hash = hexdigest(diff_file.file_path) - image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' - image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) } - .js-file-title.file-title-flex-parent{ class: is_commit ? "is-commit" : "" } + .js-file-title.file-title-flex-parent{ class: diff_page_context } .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}" diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index d66de7ab698..99cbbc11acd 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -2,7 +2,6 @@ - page_title _("Environments") #environments-list-view{ data: { environments_data: environments_list_data, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s, "can-create-environment" => can?(current_user, :create_environment, @project).to_s, "new-environment-path" => new_project_environment_path(@project), diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 8275996b522..ff7c36c2d5b 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -16,6 +16,9 @@ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + %p + - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank' + = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide } .md = brand_new_project_guidelines %p diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index 2a0ce4bd16b..6159f1c3542 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -1,7 +1,7 @@ - Gitlab::ProjectTemplate.all.each do |template| .template-option.d-flex.align-items-center - .logo.append-right-10 - = custom_icon(template.logo, size: 40) + .logo.append-right-10.px-1 + = image_tag template.logo, size: 32, class: "btn-template-icon icon-#{template.name}" .description %strong = template.title diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 5ec5a06396e..8c4d1c32ebe 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -4,10 +4,6 @@ = form_errors(@project) %fieldset.builds-feature.js-auto-devops-settings .form-group - - message = auto_devops_warning_message(@project) - - if message - %p.auto-devops-warning-message.settings-message.text-center - = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .card.auto-devops-card .card-body @@ -21,19 +17,12 @@ = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' .card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' } - = form.label :domain do - %strong= _('Domain') - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') - - if cluster_ingress_ip = cluster_ingress_ip(@project) - = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' - + %p.settings-message.text-center + - kubernetes_cluster_link = help_page_path('user/project/clusters/index') + - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } + = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe } %label.prepend-top-10 %strong= s_('CICD|Deployment strategy') - %p.settings-message.text-center - = s_('CICD|Deployment strategy needs a domain name to work correctly.') .form-check = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input' = form.label :deploy_strategy_continuous, class: 'form-check-label' do diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg deleted file mode 100644 index a51e81e5568..00000000000 --- a/app/views/shared/icons/_express.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg> diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg deleted file mode 100644 index 852bd183cc7..00000000000 --- a/app/views/shared/icons/_rails.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg> diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg deleted file mode 100644 index ccf18749029..00000000000 --- a/app/views/shared/icons/_spring.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg> diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e1564d57426..df17ae95e2a 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,21 +12,20 @@ - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) -- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block" -- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row" +- css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between" %li.project-row.d-flex{ class: css_class } = cache(cache_key) do - if avatar - .avatar-container.s64.flex-grow-0.flex-shrink-0 + .avatar-container.s48.flex-grow-0.flex-shrink-0 = link_to project_path(project), class: dom_class(project) do - if project.creator && use_creator_avatar - = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:'' + = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s65", alt:'' - else - = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64) - .project-details.flex-sm-fill{ class: css_details_class } - .flex-wrapper.flex-fill - .d-flex.align-items-center.flex-wrap + = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48) + .project-details.d-sm-flex.flex-sm-fill.align-items-center + .flex-wrapper + .d-flex.align-items-center.flex-wrap.project-title %h2.d-flex.prepend-top-8 = link_to project_path(project), class: 'text-plain' do %span.project-full-name.append-right-8>< @@ -52,13 +51,13 @@ %span.user-access-role.d-block= Gitlab::Access.human_access(access) - if show_last_commit_as_description - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") - elsif project.description.present? - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = markdown_field(project, :description) - .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } + .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } .icon-container.d-flex.align-items-center - if project.archived %span.d-flex.icon-wrapper.badge.badge-warning archived @@ -74,13 +73,13 @@ = number_with_delimiter(project.forks_count) - if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode) = link_to project_merge_requests_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip", title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do = sprite_icon('git-merge', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_merge_requests_count) - if show_issue_count?(disabled: !issues, compact_mode: compact_mode) = link_to project_issues_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip", title: _('Issues'), data: { container: 'body', placement: 'top' } do = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) @@ -89,19 +88,3 @@ = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') .updated-note %span Updated #{updated_tooltip} - - .d-none.d-lg-flex.align-item-stretch - - unless compact_mode - - if current_user - %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } } - - if current_user.starred?(project) - = sprite_icon('star', { css_class: 'icon' }) - %span.starred= s_('ProjectOverview|Unstar') - - else - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') - - - else - = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 85c123c2704..410411b1294 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -23,6 +23,7 @@ - cronjob:prune_web_hook_logs - gcp_cluster:cluster_install_app +- gcp_cluster:cluster_upgrade_app - gcp_cluster:cluster_provision - gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:wait_for_cluster_creation diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb new file mode 100644 index 00000000000..d1a538859b4 --- /dev/null +++ b/app/workers/cluster_upgrade_app_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ClusterUpgradeAppWorker + include ApplicationWorker + include ClusterQueue + include ClusterApplications + + def perform(app_name, app_id) + find_application(app_name, app_id) do |app| + Clusters::Applications::UpgradeService.new(app).execute + end + end +end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 7eae07d3f6b..a9b88a133be 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,19 +15,19 @@ class RepositoryForkWorker return target_project.import_state.mark_as_failed(_('Source project cannot be found.')) end - fork_repository(target_project, source_project.repository_storage, source_project.disk_path) + fork_repository(target_project, source_project) end private - def fork_repository(target_project, source_repository_storage_name, source_disk_path) + def fork_repository(target_project, source_project) return unless start_fork(target_project) Gitlab::Metrics.add_event(:fork_repository) - result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path, - target_project.repository_storage, target_project.disk_path) - raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result + result = gitlab_shell.fork_repository(source_project, target_project) + + raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}" unless result target_project.after_import end diff --git a/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml b/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml new file mode 100644 index 00000000000..f4a52b1aacd --- /dev/null +++ b/changelogs/unreleased/2105-add-setting-for-first-day-of-the-week.yml @@ -0,0 +1,5 @@ +--- +title: Add setting for first day of the week +merge_request: 22755 +author: Fabian Schneider @fabsrc +type: added diff --git a/changelogs/unreleased/44332-add-openid-profile-scopes.yml b/changelogs/unreleased/44332-add-openid-profile-scopes.yml new file mode 100644 index 00000000000..b554fab5139 --- /dev/null +++ b/changelogs/unreleased/44332-add-openid-profile-scopes.yml @@ -0,0 +1,5 @@ +--- +title: GitLab now supports the profile and email scopes from OpenID Connect +merge_request: 24335 +author: Goten Xiao +type: added diff --git a/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml b/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml new file mode 100644 index 00000000000..cf1c4378f18 --- /dev/null +++ b/changelogs/unreleased/52347-lines-changed-statistics-is-not-easily-visible-in-mr-changes-view.yml @@ -0,0 +1,5 @@ +--- +title: Show MR statistics in diff comparisons +merge_request: !24569 +author: +type: changed diff --git a/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml b/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml new file mode 100644 index 00000000000..eb4851971fb --- /dev/null +++ b/changelogs/unreleased/52363-ui-changes-to-cluster-and-ado-pages.yml @@ -0,0 +1,5 @@ +--- +title: Moves domain setting from Auto DevOps to Cluster's page +merge_request: 24580 +author: +type: added diff --git a/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml b/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml new file mode 100644 index 00000000000..e324baa94a3 --- /dev/null +++ b/changelogs/unreleased/56014-api-merge-request-squash-commit-messages.yml @@ -0,0 +1,5 @@ +--- +title: API allows setting the squash commit message when squashing a merge request +merge_request: 24784 +author: +type: added diff --git a/changelogs/unreleased/56014-better-squash-commit-messages.yml b/changelogs/unreleased/56014-better-squash-commit-messages.yml deleted file mode 100644 index b08d584ac0a..00000000000 --- a/changelogs/unreleased/56014-better-squash-commit-messages.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Default squash commit message is now selected from the longest commit when - squashing merge requests -merge_request: 24518 -author: -type: changed diff --git a/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml b/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml new file mode 100644 index 00000000000..388ff1d062a --- /dev/null +++ b/changelogs/unreleased/56543-project-lists-further-iteration-improvements.yml @@ -0,0 +1,5 @@ +--- +title: Project list UI improvements +merge_request: 24855 +author: +type: other diff --git a/changelogs/unreleased/56788-unicorn-metric-labels.yml b/changelogs/unreleased/56788-unicorn-metric-labels.yml new file mode 100644 index 00000000000..824c981780c --- /dev/null +++ b/changelogs/unreleased/56788-unicorn-metric-labels.yml @@ -0,0 +1,5 @@ +--- +title: Clean up unicorn sampler metric labels +merge_request: 24626 +author: bjk-gitlab +type: fixed diff --git a/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml b/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml new file mode 100644 index 00000000000..f619a009a63 --- /dev/null +++ b/changelogs/unreleased/56938-diff-file-headers-on-compare-not-quite-right.yml @@ -0,0 +1,5 @@ +--- +title: Correct spacing for comparison page +merge_request: !24783 +author: +type: fixed diff --git a/changelogs/unreleased/adriel-remove-feature-flag.yml b/changelogs/unreleased/adriel-remove-feature-flag.yml new file mode 100644 index 00000000000..d442e120d60 --- /dev/null +++ b/changelogs/unreleased/adriel-remove-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Update metrics dashboard graph design +merge_request: 24653 +author: +type: changed diff --git a/changelogs/unreleased/api-group-labels.yml b/changelogs/unreleased/api-group-labels.yml new file mode 100644 index 00000000000..0df6f15a9b6 --- /dev/null +++ b/changelogs/unreleased/api-group-labels.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Add support for group labels' +merge_request: 21368 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/fix-repo-settings-file-upload-error.yml b/changelogs/unreleased/fix-repo-settings-file-upload-error.yml new file mode 100644 index 00000000000..b219fdfaa1e --- /dev/null +++ b/changelogs/unreleased/fix-repo-settings-file-upload-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug causing repository mirror settings UI to break +merge_request: 23712 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-update-1.18.0.yml b/changelogs/unreleased/gitaly-update-1.18.0.yml new file mode 100644 index 00000000000..392527f5e5d --- /dev/null +++ b/changelogs/unreleased/gitaly-update-1.18.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade gitaly to 1.18.0 +merge_request: 24981 +author: +type: other diff --git a/changelogs/unreleased/jlenny-AddPagesTemplates.yml b/changelogs/unreleased/jlenny-AddPagesTemplates.yml new file mode 100644 index 00000000000..0985e4e18ed --- /dev/null +++ b/changelogs/unreleased/jlenny-AddPagesTemplates.yml @@ -0,0 +1,5 @@ +--- +title: Add templates for most popular Pages templates +merge_request: 24906 +author: +type: added diff --git a/changelogs/unreleased/jlenny-NewAndroidTemplate.yml b/changelogs/unreleased/jlenny-NewAndroidTemplate.yml new file mode 100644 index 00000000000..ae8c58da859 --- /dev/null +++ b/changelogs/unreleased/jlenny-NewAndroidTemplate.yml @@ -0,0 +1,5 @@ +--- +title: Add template for Android with Fastlane +merge_request: 24722 +author: +type: changed diff --git a/changelogs/unreleased/local-markdown-version-bkp3.yml b/changelogs/unreleased/local-markdown-version-bkp3.yml new file mode 100644 index 00000000000..ce5bff6ae6b --- /dev/null +++ b/changelogs/unreleased/local-markdown-version-bkp3.yml @@ -0,0 +1,5 @@ +--- +title: Allow admins to invalidate markdown texts by setting local markdown version. +merge_request: +author: +type: added diff --git a/changelogs/unreleased/move-permission-check-manual-actions-on-deployments.yml b/changelogs/unreleased/move-permission-check-manual-actions-on-deployments.yml new file mode 100644 index 00000000000..9e979b48ad1 --- /dev/null +++ b/changelogs/unreleased/move-permission-check-manual-actions-on-deployments.yml @@ -0,0 +1,5 @@ +--- +title: Move permission check of manual actions of deployments +merge_request: 24660 +author: +type: other diff --git a/changelogs/unreleased/support-chunking-in-client.yml b/changelogs/unreleased/support-chunking-in-client.yml new file mode 100644 index 00000000000..e50648ea4b2 --- /dev/null +++ b/changelogs/unreleased/support-chunking-in-client.yml @@ -0,0 +1,5 @@ +--- +title: Fix code search when text is larger than max gRPC message size +merge_request: 24111 +author: +type: changed diff --git a/changelogs/unreleased/tooltips-to-top.yml b/changelogs/unreleased/tooltips-to-top.yml new file mode 100644 index 00000000000..51bf127089e --- /dev/null +++ b/changelogs/unreleased/tooltips-to-top.yml @@ -0,0 +1,5 @@ +--- +title: Change spawning of tooltips to be top by default +merge_request: 21223 +author: +type: changed diff --git a/changelogs/unreleased/use_upgrade_install_for_helm_apps.yml b/changelogs/unreleased/use_upgrade_install_for_helm_apps.yml new file mode 100644 index 00000000000..b41c3cfa1ab --- /dev/null +++ b/changelogs/unreleased/use_upgrade_install_for_helm_apps.yml @@ -0,0 +1,5 @@ +--- +title: Added ability to upgrade cluster applications +merge_request: 24789 +author: +type: added diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb index e97c0fcbd6b..fd5a62c39c6 100644 --- a/config/initializers/doorkeeper_openid_connect.rb +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -31,8 +31,27 @@ Doorkeeper::OpenidConnect.configure do o.claim(:name) { |user| user.name } o.claim(:nickname) { |user| user.username } - o.claim(:email) { |user| user.public_email } - o.claim(:email_verified) { |user| true if user.public_email? } + + # Check whether the application has access to the email scope, and grant + # access to the user's primary email address if so, otherwise their + # public email address (if present) + # This allows existing solutions built for GitLab's old behavior to keep + # working without modification. + o.claim(:email) do |user, scopes| + scopes.exists?(:email) ? user.email : user.public_email + end + o.claim(:email_verified) do |user, scopes| + if scopes.exists?(:email) + user.primary_email_verified? + elsif user.public_email? + user.verified_email?(user.public_email) + else + # If there is no public email set, tell doorkicker-openid-connect to + # exclude the email_verified claim by returning nil. + nil + end + end + o.claim(:website) { |user| user.full_website_url if user.website_url? } o.claim(:profile) { |user| Gitlab::Routing.url_helpers.user_url user } o.claim(:picture) { |user| user.avatar_url(only_path: false) } diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 9f451046462..a2dff92908e 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -64,6 +64,8 @@ en: read_registry: Grants permission to read container registry images openid: Authenticate using OpenID Connect sudo: Perform API actions as any user in the system + profile: Allows read-only access to the user's personal information using OpenID Connect + email: Allows read-only access to the user's primary email address using OpenID Connect scope_desc: api: Grants complete read/write access to the API, including all groups and projects. @@ -77,6 +79,10 @@ en: Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships. sudo: Grants permission to perform API actions as any user in the system, when authenticated as an admin user. + profile: + Grants read-only access to the user's profile data using OpenID Connect. + email: + Grants read-only access to the user's primary email address using OpenID Connect. flash: applications: create: diff --git a/config/routes/project.rb b/config/routes/project.rb index d730479cf2b..b4ebc7df4fe 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -444,7 +444,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :error_tracking, only: [:index], controller: :error_tracking + resources :error_tracking, only: [:index], controller: :error_tracking do + collection do + post :list_projects + end + end # Since both wiki and repository routing contains wildcard characters # its preferable to keep it below all other project routes diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index a69b02cddc4..bff1f01c654 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -11,7 +11,7 @@ class MigrateRepoSize < ActiveRecord::Migration[4.2] path = File.join(namespace_path, project['project_path'] + '.git') begin - repo = Gitlab::Git::Repository.new('default', path, '') + repo = Gitlab::Git::Repository.new('default', path, '', '') if repo.empty? print '-' else diff --git a/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb b/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb new file mode 100644 index 00000000000..a0e76c2186e --- /dev/null +++ b/db/migrate/20181027114222_add_first_day_of_week_to_user_preferences.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddFirstDayOfWeekToUserPreferences < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :user_preferences, :first_day_of_week, :integer + end +end diff --git a/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb new file mode 100644 index 00000000000..53cfaa289f6 --- /dev/null +++ b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddFirstDayOfWeekToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + + def up + add_column_with_default(:application_settings, :first_day_of_week, :integer, default: 0) + end + + def down + remove_column(:application_settings, :first_day_of_week) + end +end diff --git a/db/migrate/20190130091630_add_local_cached_markdown_version.rb b/db/migrate/20190130091630_add_local_cached_markdown_version.rb new file mode 100644 index 00000000000..00570e6458c --- /dev/null +++ b/db/migrate/20190130091630_add_local_cached_markdown_version.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddLocalCachedMarkdownVersion < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, :local_markdown_version, :integer, default: 0, null: false + end +end diff --git a/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb b/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb new file mode 100644 index 00000000000..392e64eeade --- /dev/null +++ b/db/post_migrate/20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class MigrateAutoDevOpsDomainToClusterDomain < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute(update_clusters_domain_query) + end + + def down + # no-op + end + + private + + def update_clusters_domain_query + if Gitlab::Database.mysql? + mysql_query + else + postgresql_query + end + end + + def mysql_query + <<~HEREDOC + UPDATE clusters, project_auto_devops, cluster_projects + SET + clusters.domain = project_auto_devops.domain + WHERE + cluster_projects.cluster_id = clusters.id + AND project_auto_devops.project_id = cluster_projects.project_id + AND project_auto_devops.domain != '' + HEREDOC + end + + def postgresql_query + <<~HEREDOC + UPDATE clusters + SET domain = project_auto_devops.domain + FROM cluster_projects, project_auto_devops + WHERE + cluster_projects.cluster_id = clusters.id + AND project_auto_devops.project_id = cluster_projects.project_id + AND project_auto_devops.domain != '' + HEREDOC + end +end diff --git a/db/schema.rb b/db/schema.rb index 20c8dab4c3e..023eee5f33e 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: 20190131122559) do +ActiveRecord::Schema.define(version: 20190204115450) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -168,6 +168,8 @@ ActiveRecord::Schema.define(version: 20190131122559) do t.string "commit_email_hostname" t.boolean "protected_ci_variables", default: false, null: false t.string "runners_registration_token_encrypted" + t.integer "local_markdown_version", default: 0, null: false + t.integer "first_day_of_week", default: 0, null: false t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree end @@ -2153,6 +2155,7 @@ ActiveRecord::Schema.define(version: 20190131122559) do t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false + t.integer "first_day_of_week" t.string "issues_sort" t.string "merge_requests_sort" t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree diff --git a/doc/README.md b/doc/README.md index 1a0359f9e2a..f87ff925e94 100644 --- a/doc/README.md +++ b/doc/README.md @@ -336,7 +336,7 @@ Viewing [Container Scanning reports](https://docs.gitlab.com/ee/user/project/mer There are two ways to use GitLab: - [GitLab self-managed](#gitlab-self-managed): Install, administer, and maintain your own GitLab instance. -- [GitLab.com](#gitlab-com): GitLab's SaaS offering. You don't need to install anything to use GitLab.com, +- [GitLab.com](#gitlabcom): GitLab's SaaS offering. You don't need to install anything to use GitLab.com, you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab straight away. The following sections outline tiers and features within GitLab self-managed and GitLab.com. diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index abef7a6cd33..0795d3dad40 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -52,10 +52,10 @@ is used. ### Network architecture - gitlab-rails shards repositories into "repository storages" -- gitlab-rails/config/gitlab.yml contains a map from storage names to +- `gitlab-rails/config/gitlab.yml` contains a map from storage names to (Gitaly address, Gitaly token) pairs - the `storage name` -\> `(Gitaly address, Gitaly token)` map in - gitlab.yml is the single source of truth for the Gitaly network + `gitlab.yml` is the single source of truth for the Gitaly network topology - a (Gitaly address, Gitaly token) corresponds to a Gitaly server - a Gitaly server hosts one or more storages @@ -65,7 +65,7 @@ is used. gitlab-shell, and Gitaly itself - special case: a Gitaly server must be able to make RPC calls **to itself** via its own (Gitaly address, Gitaly token) pair as - specified in gitlab-rails/config/gitlab.yml + specified in `gitlab-rails/config/gitlab.yml` - Gitaly servers must not be exposed to the public internet Gitaly network traffic is unencrypted so you should use a firewall to @@ -125,7 +125,7 @@ Omnibus installations: ```ruby # /etc/gitlab/gitlab.rb -# Avoid running unnecessary services on the gitaly server +# Avoid running unnecessary services on the Gitaly server postgresql['enable'] = false redis['enable'] = false nginx['enable'] = false @@ -153,7 +153,7 @@ gitaly['storage'] = [ { 'name' => 'storage1', 'path' => '/mnt/gitlab/storage1/repositories' }, ] -# To use tls for gitaly you need to add +# To use TLS for Gitaly you need to add gitaly['tls_listen_addr'] = "0.0.0.0:9999" gitaly['certificate_path'] = "path/to/cert.pem" gitaly['key_path'] = "path/to/key.pem" @@ -239,11 +239,11 @@ repository from your GitLab server over HTTP. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22602) in GitLab 11.7. Gitaly supports TLS credentials for GRPC authentication. To be able to communicate -with a gitaly instance that listens for secure connections you will need to use `tls://` url +with a Gitaly instance that listens for secure connections you will need to use `tls://` url scheme in the `gitaly_address` of the corresponding storage entry in the gitlab configuration. The admin needs to bring their own certificate as we do not provide that automatically. -The certificate to be used needs to be installed on all gitaly nodes and on all client nodes that communicate with it following procedures described in [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) +The certificate to be used needs to be installed on all Gitaly nodes and on all client nodes that communicate with it following procedures described in [GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates) ### Example TLS configuration @@ -261,7 +261,7 @@ git_data_dirs({ gitlab_rails['gitaly_token'] = 'abc123secret' ``` -#### On gitaly server nodes: +#### On Gitaly server nodes: ```ruby gitaly['tls_listen_addr'] = "0.0.0.0:9999" @@ -289,7 +289,7 @@ gitlab: token: 'abc123secret' ``` -#### On gitaly server nodes: +#### On Gitaly server nodes: ```toml # /home/git/gitaly/config.toml diff --git a/doc/administration/index.md b/doc/administration/index.md index 184754cd467..12fec2753bf 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -65,6 +65,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance. - [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq MemoryKiller, Unicorn). - [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components. +- [Invalidate markdown cache](invalidate_markdown_cache.md): Invalidate any cached markdown. #### Updating GitLab diff --git a/doc/administration/invalidate_markdown_cache.md b/doc/administration/invalidate_markdown_cache.md new file mode 100644 index 00000000000..ad64cb077c1 --- /dev/null +++ b/doc/administration/invalidate_markdown_cache.md @@ -0,0 +1,16 @@ +# Invalidate Markdown Cache + +For performance reasons, GitLab caches the HTML version of markdown text +(e.g. issue and merge request descriptions, comments). It's possible +that these cached versions become outdated, for example +when the `external_url` configuration option is changed - causing links +in the cached text to refer to the old URL. + +To avoid this problem, the administrator can invalidate the existing cache by +increasing the `local_markdown_version` setting in application settings. This can +be done by [changing the application settings through +the API](../api/settings.md#change-application-settings): + +```bash +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/application/settings?local_markdown_version=<increased_number> +``` diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index c9a2778b3a4..6ea0ac0d495 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -48,6 +48,8 @@ The following metrics are available: | upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file | | failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login | | successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login | +| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) | +| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections | ### Ruby metrics diff --git a/doc/administration/operations/filesystem_benchmarking.md b/doc/administration/operations/filesystem_benchmarking.md index 0397452e650..c0c242733a2 100644 --- a/doc/administration/operations/filesystem_benchmarking.md +++ b/doc/administration/operations/filesystem_benchmarking.md @@ -7,13 +7,72 @@ systems. Normally when talking about filesystem performance the biggest concern is with Network Filesystems (NFS). However, even some local disks can have slow -IO. The information on this page can be used for either scenario. +I/O. The information on this page can be used for either scenario. -## Write Performance +## Executing benchmarks -The following one-line command is a quick benchmark for filesystem write +### Benchmarking with `fio` + +We recommend using +[fio](https://fio.readthedocs.io/en/latest/fio_doc.html) to test I/O +performance. This test should be run both on the NFS server and on the +application nodes that talk to the NFS server. + +To install: + +- On Ubuntu: `apt install fio`. +- On `yum`-managed environments: `yum install fio`. + +Then run the following: + +```sh +fio --randrepeat=1 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=test --filename=/path/to/git-data/testfile --bs=4k --iodepth=64 --size=4G --readwrite=randrw --rwmixread=75 +``` + +This will create a 4GB file in `/path/to/git-data/testfile`. It performs +4KB reads and writes using a 75%/25% split within the file, with 64 +operations running at a time. Be sure to delete the file after the test +completes. + +The output will vary depending on what version of `fio` installed. The following +is an example output from `fio` v2.2.10 on a networked solid-state drive (SSD): + +``` +test: (g=0): rw=randrw, bs=4K-4K/4K-4K/4K-4K, ioengine=libaio, iodepth=64 + fio-2.2.10 + Starting 1 process + test: Laying out IO file(s) (1 file(s) / 1024MB) + Jobs: 1 (f=1): [m(1)] [100.0% done] [131.4MB/44868KB/0KB /s] [33.7K/11.3K/0 iops] [eta 00m:00s] + test: (groupid=0, jobs=1): err= 0: pid=10287: Sat Feb 2 17:40:10 2019 + read : io=784996KB, bw=133662KB/s, iops=33415, runt= 5873msec + write: io=263580KB, bw=44880KB/s, iops=11219, runt= 5873msec + cpu : usr=6.56%, sys=23.11%, ctx=266267, majf=0, minf=8 + IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0% + submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0% + complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0% + issued : total=r=196249/w=65895/d=0, short=r=0/w=0/d=0, drop=r=0/w=0/d=0 + latency : target=0, window=0, percentile=100.00%, depth=64 + + Run status group 0 (all jobs): + READ: io=784996KB, aggrb=133661KB/s, minb=133661KB/s, maxb=133661KB/s, mint=5873msec, maxt=5873msec + WRITE: io=263580KB, aggrb=44879KB/s, minb=44879KB/s, maxb=44879KB/s, mint=5873msec, maxt=5873msec +``` + +Notice the `iops` values in this output. In this example, the SSD +performed 33,415 read operations per second and 11,219 write operations +per second. A spinning disk might yield 2,000 and 700 read and write +operations per second. + +### Simple benchmarking + +NOTE: **Note:** This test is naive but may be useful if `fio` is not +available on the system. It's possible to receive good results on this +test but still have poor performance due to read speed and various other +factors. + +The following one-line commands provide a quick benchmark for filesystem write and read performance. This will write 1,000 small files to the directory in which it is -executed. +executed, and then read the same 1,000 files. 1. Change into the root of the appropriate [repository storage path](../repository_storage_paths.md). @@ -27,13 +86,18 @@ executed. ```sh time for i in {0..1000}; do echo 'test' > "test${i}.txt"; done ``` +1. To benchmark read performance, run the command: + + ```sh + time for i in {0..1000}; do cat "test${i}.txt" > /dev/null; done + ``` 1. Remove the test files: ```sh cd ../; rm -rf test ``` -The output of the `time for ...` command will look similar to the following. The +The output of the `time for ...` commands will look similar to the following. The important metric is the `real` time. ```sh @@ -42,12 +106,13 @@ $ time for i in {0..1000}; do echo 'test' > "test${i}.txt"; done real 0m0.116s user 0m0.025s sys 0m0.091s + +$ time for i in {0..1000}; do cat "test${i}.txt" > /dev/null; done + +real 0m3.118s +user 0m1.267s +sys 0m1.663s ``` From experience with multiple customers, this task should take under 10 -seconds to indicate good filesystem performance. - -NOTE: **Note:** -This test is naive and only evaluates write performance. It's possible to -receive good results on this test but still have poor performance due to read -speed and various other factors.
\ No newline at end of file +seconds to indicate good filesystem performance. diff --git a/doc/api/README.md b/doc/api/README.md index 692f63a400c..a060e0481bf 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,6 +29,7 @@ The following API resources are available: - [Group access requests](access_requests.md) - [Group badges](group_badges.md) - [Group issue boards](group_boards.md) + - [Group labels](group_labels.md) - [Group-level variables](group_level_variables.md) - [Group members](members.md) - [Group milestones](group_milestones.md) diff --git a/doc/api/group_labels.md b/doc/api/group_labels.md new file mode 100644 index 00000000000..c36d34b4af1 --- /dev/null +++ b/doc/api/group_labels.md @@ -0,0 +1,201 @@ +# Group Label API + +>**Note:** This feature was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21368) in GitLab 11.8. + +This API supports managing of [group labels](../user/project/labels.md#project-labels-and-group-labels). It allows to list, create, update, and delete group labels. Furthermore, users can subscribe and unsubscribe to and from group labels. + +## List group labels + +Get all labels for a given group. + +``` +GET /groups/:id/labels +``` + +| 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 | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels +``` + +Example response: + +```json +[ + { + "id": 7, + "name": "bug", + "color": "#FF0000", + "description": null, + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false + }, + { + "id": 4, + "name": "feature", + "color": "#228B22", + "description": null, + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false + } +] +``` + +## Create a new group label + +Create a new group label for a given group. + +``` +POST /groups/:id/labels +``` + +| 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 | yes | The name of the label | +| `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) | +| `description` | string | no | The description of the label | + +```bash +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "Feature Proposal", "color": "#FFA500", "description": "Describes new ideas" }' https://gitlab.example.com/api/v4/groups/5/labels +``` + +Example response: + +```json +{ + "id": 9, + "name": "Feature Proposal", + "color": "#FFA500", + "description": "Describes new ideas", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` + +## Update a group label + +Updates an existing group label. At least one parameter is required, to update the group label. + +``` +PUT /groups/:id/labels +``` + +| 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 | yes | The name of the label | +| `new_name` | string | no | The new name of the label | +| `color` | string | no | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) | +| `description` | string | no | The description of the label | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "Feature Proposal", "new_name": "Feature Idea" }' https://gitlab.example.com/api/v4/groups/5/labels +``` + +Example response: + +```json +{ + "id": 9, + "name": "Feature Idea", + "color": "#FFA500", + "description": "Describes new ideas", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` + +## Delete a group label + +Deletes a group label with a given name. + +``` +DELETE /groups/:id/labels +``` + +| 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 | yes | The name of the label | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels?name=bug +``` + +## Subscribe to a group label + +Subscribes the authenticated user to a group label to receive notifications. If +the user is already subscribed to the label, the status code `304` is returned. + +``` +POST /groups/:id/labels/:label_id/subscribe +``` + +| 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 | +| `label_id` | integer or string | yes | The ID or title of a group's label | + +```bash +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels/9/subscribe +``` + +Example response: + +```json +{ + "id": 9, + "name": "Feature Idea", + "color": "#FFA500", + "description": "Describes new ideas", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": true +} +``` + +## Unsubscribe from a group label + +Unsubscribes the authenticated user from a group label to not receive +notifications from it. If the user is not subscribed to the label, the status +code `304` is returned. + +``` +POST /groups/:id/labels/:label_id/unsubscribe +``` + +| 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 | +| `label_id` | integer or string | yes | The ID or title of a group's label | + +```bash +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels/9/unsubscribe +``` + +Example response: + +```json +{ + "id": 9, + "name": "Feature Idea", + "color": "#FFA500", + "description": "Describes new ideas", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 802ff1d1df9..d58cd45538d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -994,6 +994,8 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `merge_request_iid` (required) - Internal ID of MR - `merge_commit_message` (optional) - Custom merge commit message +- `squash_commit_message` (optional) - Custom squash commit message +- `squash` (optional) - if `true` the commits will be squashed into a single commit on merge - `should_remove_source_branch` (optional) - if `true` removes the source branch - `merge_when_pipeline_succeeds` (optional) - if `true` the MR is merged when the pipeline succeeds - `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail diff --git a/doc/api/settings.md b/doc/api/settings.md index c329e3cdf24..2e0a2a09133 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -57,11 +57,13 @@ Example response: "dsa_key_restriction": 0, "ecdsa_key_restriction": 0, "ed25519_key_restriction": 0, + "first_day_of_week": 0, "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, "instance_statistics_visibility_private": false, - "user_show_add_ssh_key_message": true + "user_show_add_ssh_key_message": true, + "local_markdown_version": 0 } ``` @@ -113,11 +115,13 @@ Example response: "dsa_key_restriction": 0, "ecdsa_key_restriction": 0, "ed25519_key_restriction": 0, + "first_day_of_week": 0, "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, "instance_statistics_visibility_private": false, - "user_show_add_ssh_key_message": true + "user_show_add_ssh_key_message": true, + "local_markdown_version": 0 } ``` @@ -157,6 +161,7 @@ are listed in the descriptions of the relevant settings. | `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | | `enforce_terms` | boolean | no | (**If enabled, requires:** `terms`) Enforce application ToS to all users. | +| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday and `1` for Monday. | | `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | | `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. | | `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. | @@ -235,3 +240,4 @@ are listed in the descriptions of the relevant settings. | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider. | | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | +| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 97e133a2e2f..32c73c4f398 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -98,7 +98,7 @@ future GitLab releases.** | **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` | | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL | -| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | +| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | | **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) | | **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 24feb1378a1..c5344139ab4 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -20,7 +20,7 @@ All labels, their meaning and priority are defined on the If you come across an issue that has none of these, and you're allowed to set labels, you can _always_ add the team and type, and often also the subject. -[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones +[milestones-page]: https://gitlab.com/groups/gitlab-org/-/milestones ## Type labels diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 9bef0635e3f..19b6181c9a2 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -86,6 +86,9 @@ request is as follows: guidelines](../merge_request_performance_guidelines.md). 1. For tests that use Capybara or PhantomJS, see this [article on how to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara). +1. If your merge request introduces changes that require additional steps when + installing GitLab from source, add them to `doc/install/installation.md` in + the same merge request. Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 00db58a45a2..223585ebb55 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -231,11 +231,11 @@ This makes use of [`Intl.DateTimeFormat`]. - In Ruby/HAML, we have two ways of adding format to dates and times: 1. **Through the `l` helper**, i.e. `l(active_session.created_at, format: :short)`. We have some predefined formats for -[dates](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L54) and [times](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L261). + [dates](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L54) and [times](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L261). If you need to add a new format, because other parts of the code could benefit from it, you'll need to add it to [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) file. - 2. **Through `strftime`**, i.e. `milestone.start_date.strftime('%b %-d')`. We use `strftime` in case none of the formats - defined on [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) matches the date/time + 1. **Through `strftime`**, i.e. `milestone.start_date.strftime('%b %-d')`. We use `strftime` in case none of the formats + defined on [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) matches the date/time specifications we need, and if there is no need to add it as a new format because is very particular (i.e. it's only used in a single view). ## Best practices diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md index c44690a4c5d..5e4923341af 100644 --- a/doc/development/i18n/index.md +++ b/doc/development/i18n/index.md @@ -51,4 +51,4 @@ able to proofread and instructions on becoming a proofreader yourself. Translations are typically included in the next major or minor release. -See [Merging translations from Crowdin](merging_translations.md) +See [Merging translations from Crowdin](merging_translations.md). diff --git a/doc/development/i18n/merging_translations.md b/doc/development/i18n/merging_translations.md index d172aa6da21..2fa7558d30b 100644 --- a/doc/development/i18n/merging_translations.md +++ b/doc/development/i18n/merging_translations.md @@ -4,7 +4,7 @@ Crowdin automatically syncs the `gitlab.pot` file presenting newly added translations to the community of translators. At the same time, it creates a merge request to merge all newly added -& approved translations. Find the [merge reqeust created by +& approved translations. Find the [merge request created by `gitlab-crowdin-bot`](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&author_username=gitlab-crowdin-bot) to see new and merged merge requests. They are created in EE and need to be ported to CE manually. diff --git a/doc/install/installation.md b/doc/install/installation.md index 45a76bd1d01..a8064ae046e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -345,11 +345,15 @@ cd /home/git ```sh # Clone GitLab repository -sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b X-Y-stable gitlab ``` +Make sure to replace `X-Y-stable` with the stable branch that matches the +version you want to install. For example, if you want to install 11.8 you would +use the branch name `11-8-stable`. + CAUTION: **Caution:** -You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install `master` on a production server! +You can change `X-Y-stable` to `master` if you want the *bleeding edge* version, but never install `master` on a production server! ### Configure It @@ -691,6 +695,11 @@ sudo nginx -t You should receive `syntax is okay` and `test is successful` messages. If you receive errors check your `gitlab` or `gitlab-ssl` Nginx config file for typos, etc. as indicated in the error message given. +NOTE: **Note:** +Verify that the installed version is greater than 1.12.1 by running `nginx -v`. If it's lower, you may receive the error below: +`nginx: [emerg] unknown "start$temp=[filtered]$rest" variable +nginx: configuration file /etc/nginx/nginx.conf test failed` + ### Restart ```sh diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 80de39c207a..1b53be15b44 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -114,7 +114,8 @@ To create a new SSH key pair: and want to tell which is which. It is optional. 1. Next, you will be prompted to input a file path to save your SSH key pair to. - If you don't already have an SSH key pair, use the suggested path by pressing + If you don't already have an SSH key pair and aren't generating a [deploy key](#deploy-keys), + use the suggested path by pressing <kbd>Enter</kbd>. Using the suggested path will normally allow your SSH client to automatically use the SSH key pair with no additional configuration. @@ -128,7 +129,7 @@ To create a new SSH key pair: <kbd>Enter</kbd> twice. If, in any case, you want to add or change the password of your SSH key pair, - you can use the `-p`flag: + you can use the `-p` flag: ``` ssh-keygen -p -o -f <keyname> @@ -258,7 +259,8 @@ Integration (CI) server. By using deploy keys, you don't have to set up a dummy user account. If you are a project maintainer or owner, you can add a deploy key in the -project settings under the section 'Repository'. Specify a title for the new +project's **Settings > Repository** page by expanding the +**Deploy Keys** section. Specify a title for the new deploy key and paste a public SSH key. After this, the machine that uses the corresponding private SSH key has read-only or read-write (if enabled) access to the project. @@ -300,8 +302,8 @@ of broader usage for something like "Anywhere you need to give read access to your repository". Once a GitLab administrator adds the Global Deployment key, project maintainers -and owners can add it in project's **Settings > Repository** section by expanding the -**Deploy Key** section and clicking **Enable** next to the appropriate key listed +and owners can add it in project's **Settings > Repository** page by expanding the +**Deploy Keys** section and clicking **Enable** next to the appropriate key listed under **Public deploy keys available to any project**. NOTE: **Note:** diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 325de50cab0..463bdd59282 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -126,14 +126,22 @@ Auto Deploy, and Auto Monitoring will be silently skipped. ## Auto DevOps base domain +NOTE: **Note** +`AUTO_DEVOPS_DOMAIN` environment variable is deprecated and +[is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959) in GitLab 12.0. + The Auto DevOps base domain is required if you want to make use of [Auto Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined -in three places: +in any of the following places: -- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) +- either under the cluster's settings, whether for [projects](../../user/project/clusters/index.md#base-domain) or [groups](../../user/group/clusters/index.md#base-domain) - or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section -- or at the project as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters)) -- or at the group level as a variable: `AUTO_DEVOPS_DOMAIN` +- or at the project level as a variable: `KUBE_INGRESS_BASE_DOMAIN` +- or at the group level as a variable: `KUBE_INGRESS_BASE_DOMAIN`. + +NOTE: **Note** +The Auto DevOps base domain variable (`KUBE_INGRESS_BASE_DOMAIN`) follows the same order of precedence +as other environment [variables](../../ci/variables/README.md#priority-of-variables). A wildcard DNS A record matching the base domain(s) is required, for example, given a base domain of `example.com`, you'd need a DNS entry like: @@ -170,13 +178,13 @@ In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ce/blob/maste Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so except for the environment scope, they would also need to have a different domain they would be deployed to. This is why you need to define a separate -`AUTO_DEVOPS_DOMAIN` variable for all the above +`KUBE_INGRESS_BASE_DOMAIN` variable for all the above [based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables). The following table is an example of how the three different clusters would be configured. -| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes | +| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` variable value | Variable environment scope | Notes | | ------------ | -------------- | ----------------------------- | ------------- | ------ | | review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. | | staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). | @@ -190,14 +198,11 @@ To add a different cluster for each environment: ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png) 1. After the clusters are created, navigate to each one and install Helm Tiller - and Ingress. + and Ingress. Wait for the Ingress IP address to be assigned. 1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the specified Auto DevOps domains. -1. Navigate to your project's **Settings > CI/CD > Environment variables** and add - the `AUTO_DEVOPS_DOMAIN` variables with their respective environment - scope. - - ![Auto DevOps domain variables](img/autodevops_domain_variables.png) +1. Navigate to each cluster's page, through **Operations > Kubernetes**, + and add the domain based on its Ingress IP address. Now that all is configured, you can test your setup by creating a merge request and verifying that your app is deployed as a review app in the Kubernetes @@ -205,10 +210,9 @@ cluster with the `review/*` environment scope. Similarly, you can check the other environments. NOTE: **Note:** -Auto DevOps is not supported for a group with multiple clusters, as it -is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group -level. This will be resolved in the future with the [following issue]( -https://gitlab.com/gitlab-org/gitlab-ce/issues/52363). +From GitLab 11.8, `KUBE_INGRESS_BASE_DOMAIN` replaces `AUTO_DEVOPS_DOMAIN`. +`AUTO_DEVOPS_DOMAIN` [is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959) +in GitLab 12.0. ## Enabling/Disabling Auto DevOps @@ -681,7 +685,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | **Variable** | **Description** | | ------------ | --------------- | -| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain); by default set automatically by the [Auto DevOps setting](#enabling-auto-devops). | +| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-domain). By default, set automatically by the [Auto DevOps setting](#enabling-auto-devops). This variable is deprecated and [is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959) in GitLab 12.0. Use `KUBE_INGRESS_BASE_DOMAIN` instead. | | `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/auto-deploy-app). | | `AUTO_DEVOPS_CHART_REPOSITORY` | The Helm Chart repository used to search for charts; defaults to `https://charts.gitlab.io`. | | `REPLICAS` | The number of replicas to deploy; defaults to 1. | @@ -711,6 +715,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. | | `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. | | `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. | +| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. | TIP: **Tip:** Set up the replica variables using a diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 9f9b2da23e1..9fc50741407 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -59,11 +59,16 @@ Add another cluster similar to the first one and make sure to [set an environment scope](#environment-scopes) that will differentiate the new cluster from the rest. -NOTE: **Note:** -Auto DevOps is not supported for a group with multiple clusters, as it -is not possible to set `AUTO_DEVOPS_DOMAIN` per environment on the group -level. This will be resolved in the future with the [following issue]( -https://gitlab.com/gitlab-org/gitlab-ce/issues/52363). +## Base domain + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8. + +Domains at the cluster level permit support for multiple domains +per [multiple Kubernetes clusters](#multiple-kubernetes-clusters-premium). When specifying a domain, +this will be automatically set as an environment variable (`KUBE_INGRESS_BASE_DOMAIN`) during +the [Auto DevOps](../../../topics/autodevops/index.md) stages. + +The domain should have a wildcard DNS configured to the Ingress IP address. ## Environment scopes **[PREMIUM]** diff --git a/doc/user/index.md b/doc/user/index.md index fc68404d0c2..36aba5e01c6 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -12,28 +12,22 @@ includes, except [GitLab administrator](../README.md#administrator-documentation settings, unless you have admin privileges to install, configure, and upgrade your GitLab instance. -For GitLab.com, admin privileges are restricted to the GitLab team. +Admin privileges for [GitLab.com](https://gitlab.com/) are restricted to the GitLab team. -If you run your own GitLab instance and are looking for the administration settings, -please refer to the [administration](../README.md#administrator-documentation) -documentation. +For more information on configuring GitLab self-managed instances, see [Administrator documentation](../README.md#administrator-documentation). ## Overview -GitLab is a fully integrated software development platform that enables you -and your team to work cohesively, faster, transparently, and effectively, -since the discussion of a new idea until taking that idea to production all -the way through, from within the same platform. +GitLab is a fully integrated software development platform that enables your team to be transparent, fast, effective, and cohesive from discussion on a new idea to production, all on the same platform. -Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/). +For more information, see [All GitLab Features](https://about.gitlab.com/features/). ### Concepts -For an overview on concepts involved when developing code on GitLab, -read the articles on: +To get familiar with the concepts needed to develop code on GitLab, read the following articles: -- [Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/). -- [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). +- [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/). +- [GitLab Workflow: An Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). - [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/): an overview on code collaboration with GitLab. - [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/). - [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/). @@ -42,16 +36,16 @@ read the articles on: GitLab is a Git-based platform that integrates a great number of essential tools for software development and deployment, and project management: -- Code hosting in repositories with version control -- Track proposals for new implementations, bug reports, and feedback with a +- Hosting code in repositories with version control +- Tracking proposals for new implementations, bug reports, and feedback with a fully featured [Issue Tracker](project/issues/index.md#issue-tracker) -- Organize and prioritize with [Issue Boards](project/issues/index.md#issue-boards) -- Code review in [Merge Requests](project/merge_requests/index.md) with live-preview changes per +- Organizing and prioritizing with [Issue Boards](project/issues/index.md#issue-boards) +- Reviewing code in [Merge Requests](project/merge_requests/index.md) with live-preview changes per branch with [Review Apps](../ci/review_apps/index.md) -- Build, test and deploy with built-in [Continuous Integration](../ci/README.md) -- Deploy your personal and professional static websites with [GitLab Pages](project/pages/index.md) -- Integrate with Docker with [GitLab Container Registry](project/container_registry.md) -- Track the development lifecycle with [GitLab Cycle Analytics](project/cycle_analytics.md) +- Building, testing and deploying with built-in [Continuous Integration](../ci/README.md) +- Deploying personal and professional static websites with [GitLab Pages](project/pages/index.md) +- Integrating with Docker by using [GitLab Container Registry](project/container_registry.md) +- Tracking the development lifecycle by usingn [GitLab Cycle Analytics](project/cycle_analytics.md) With GitLab Enterprise Edition, you can also: @@ -68,15 +62,15 @@ and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board. - [Export issues as CSV](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) - View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) - [Lock files](https://docs.gitlab.com/ee/user/project/file_lock.html) to prevent conflicts -- View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) -- Leverage your continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) +- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) +- Leverage continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more. ## Projects -In GitLab, you can create [projects](project/index.md) for numerous reasons, such as, host -your code, use it as an issue tracker, collaborate on code, and continuously +In GitLab, you can create [projects](project/index.md) to host +your code, track issues, collaborate on code, and continuously build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do it all at once, from one single project. diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index eb2d731343e..363d3db8db1 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -87,3 +87,11 @@ You can choose between 3 options: - Files and Readme (default) - Readme - Activity + +## Localization + +### First day of the week + +The first day of the week can be customised for calendar views and date pickers. + +You can choose **Sunday** or **Monday** as the first day of the week. If you select **System Default**, the system-wide default setting will be used. diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index bb815695cb1..85a4af24dc5 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -172,6 +172,17 @@ functionalities needed to successfully build and deploy a containerized application. Bear in mind that the same credentials are used for all the applications running on the cluster. +## Base domain + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8. + +Domains at the cluster level permit support for multiple domains +per [multiple Kubernetes clusters](#multiple-kubernetes-clusters-premium). When specifying a domain, +this will be automatically set as an environment variable (`KUBE_INGRESS_BASE_DOMAIN`) during +the [Auto DevOps](../../../topics/autodevops/index.md) stages. + +The domain should have a wildcard DNS configured to the Ingress IP address. + ## Access controls When creating a cluster in GitLab, you will be asked if you would like to create an @@ -254,6 +265,12 @@ install it manually. ## Installing applications +NOTE: **Note:** +Before starting the installation of applications, make sure that time is synchronized +between your GitLab server and your Kubernetes cluster. Otherwise, installation could fail +and you may get errors like `Error: remote error: tls: bad certificate` +in the `stdout` of pods created by GitLab in your Kubernetes cluster. + GitLab provides a one-click install for various applications which can be added directly to your configured cluster. Those applications are needed for [Review Apps](../../../ci/review_apps/index.md) and @@ -449,6 +466,7 @@ GitLab CI/CD build environment. | `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. | | `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. | | `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. | +| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. | NOTE: **NOTE:** Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main diff --git a/doc/user/project/merge_requests/img/squash_mr_message.png b/doc/user/project/merge_requests/img/squash_mr_message.png Binary files differnew file mode 100644 index 00000000000..8734cab29aa --- /dev/null +++ b/doc/user/project/merge_requests/img/squash_mr_message.png diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md index 34cba867e2c..4ff8ec3a7e6 100644 --- a/doc/user/project/merge_requests/squash_and_merge.md +++ b/doc/user/project/merge_requests/squash_and_merge.md @@ -23,11 +23,14 @@ The squashed commit's commit message will be either: - Taken from the first multi-line commit message in the merge. - The merge request's title if no multi-line commit message is found. -Note that the squashed commit is still followed by a merge commit, -as the merge method for this example repository uses a merge commit. -Squashing also works with the fast-forward merge strategy, see -[squashing and fast-forward merge](#squash-and-fast-forward-merge) for more -details. +It can be customized before merging a merge request. + +![A squash commit message editor](img/squash_mr_message.png) + +NOTE: **Note:** +The squashed commit in this example is followed by a merge commit, as the merge method for this example repository uses a merge commit. + +Squashing also works with the fast-forward merge strategy, see [squashing and fast-forward merge](#squash-and-fast-forward-merge) for more details. ## Use cases @@ -60,7 +63,7 @@ This can then be overridden at the time of accepting the merge request: The squashed commit has the following metadata: -- Message: the message of the squash commit. +- Message: the message of the squash commit, or a customized message. - Author: the author of the merge request. - Committer: the user who initiated the squash. diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index 68dd3330d7a..b2da1c85c62 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -79,11 +79,14 @@ running on your instance). ![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated_2018.png) -NOTE: **Note:** -Note that if you use your root domain for your GitLab Pages website **only**, and if -your domain registrar supports this feature, you can add a DNS apex `CNAME` -record instead of an `A` record. The main advantage of doing so is that when GitLab Pages -IP on GitLab.com changes for whatever reason, you don't need to update your `A` record. +CAUTION: **Caution:** +Note that if you use your root domain for your GitLab Pages website +**only**, and if your domain registrar supports this feature, you can +add a DNS apex `CNAME` record instead of an `A` record. The main +advantage of doing so is that when GitLab Pages IP on GitLab.com +changes for whatever reason, you don't need to update your `A` record. +There may be a few exceptions, but **this method is not recommended** +as it most likely won't work if you set an `MX` record for your root domain. #### DNS CNAME record @@ -114,14 +117,16 @@ co-exist, so you need to place the TXT record in a special subdomain of its own. #### TL;DR -If the domain has multiple uses (e.g., you host email on it as well): +For root domains (`domain.com`), set a DNS `A` record and verify your +domain's ownership with a TXT record: | From | DNS Record | To | | ---- | ---------- | -- | | domain.com | A | 35.185.44.232 | | domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff | -If the domain is dedicated to GitLab Pages use and no other services run on it: +For subdomains (`subdomain.domain.com`), set a DNS `CNAME` record and +verify your domain's ownership with a TXT record: | From | DNS Record | To | | ---- | ---------- | -- | diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index 8dbbdd051f1..c9081a6d72b 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -71,7 +71,7 @@ created for the steps below. To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to: - Rename it to `namespace.gitlab.io`: navigate to project's **Settings** > expand **Advanced settings** > and scroll down to **Rename repository** -- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likely, it will be in the SSG's config file. +- Adjust your SSG's [base URL](#urls-and-baseurls) from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likely, it will be in the SSG's config file. > **Notes:** > diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index a8b47558c99..0d0575b1ab4 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -38,8 +38,9 @@ turn are defined with the `paths` keyword. All paths to files and directories are relative to the repository that was cloned during the build. These uploaded artifacts will be kept in GitLab for 1 week as defined by the `expire_in` definition. You have the option to keep the artifacts from expiring via the -[web interface](#browsing-job-artifacts). If you don't define an expiry date, -the artifacts will be kept forever. +[web interface](#browsing-job-artifacts). If the expiry time is not defined, +it defaults to the [instance wide +setting](../../admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only). For more examples on artifacts, follow the [artifacts reference in `.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts). diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 1213474b7d8..8a2f4e1b40e 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -88,6 +88,14 @@ The mirrored repository will be listed. For example, `https://*****:*****@github The repository will push soon. To force a push, click the appropriate button. +## Setting up a push mirror to another GitLab instance with 2FA activated + +1. On the destination GitLab instance, create a [personal access token](../user/profile/personal_access_tokens.md) with `API` scope. +1. On the source GitLab instance: + 1. Fill in the **Git repository URL** field using this format: `https://oauth2@<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`. + 1. Fill in **Password** field with the GitLab personal access token created on the destination GitLab instance. + 1. Click the **Mirror repository** button. + ## Pulling from a remote repository **[STARTER]** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. diff --git a/lib/api/api.rb b/lib/api/api.rb index 9cbfc0e35ff..4dd1b459554 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -109,6 +109,7 @@ module API mount ::API::Features mount ::API::Files mount ::API::GroupBoards + mount ::API::GroupLabels mount ::API::GroupMilestones mount ::API::Groups mount ::API::GroupVariables diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a1f0efa3c68..beb8ce349b4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1019,12 +1019,17 @@ module API label.open_merge_requests_count(options[:current_user]) end - expose :priority do |label, options| - label.priority(options[:project]) + expose :subscribed do |label, options| + label.subscribed?(options[:current_user], options[:parent]) end + end - expose :subscribed do |label, options| - label.subscribed?(options[:current_user], options[:project]) + class GroupLabel < Label + end + + class ProjectLabel < Label + expose :priority do |label, options| + label.priority(options[:parent]) end end diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb new file mode 100644 index 00000000000..0dbc5f45a68 --- /dev/null +++ b/lib/api/group_labels.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module API + class GroupLabels < Grape::API + include PaginationParams + helpers ::API::Helpers::LabelHelpers + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get all labels of the group' do + detail 'This feature was added in GitLab 11.8' + success Entities::GroupLabel + end + params do + use :pagination + end + get ':id/labels' do + get_labels(user_group, Entities::GroupLabel) + end + + desc 'Create a new label' do + detail 'This feature was added in GitLab 11.8' + success Entities::GroupLabel + end + params do + use :label_create_params + end + post ':id/labels' do + create_label(user_group, Entities::GroupLabel) + end + + desc 'Update an existing label. At least one optional parameter is required.' do + detail 'This feature was added in GitLab 11.8' + success Entities::GroupLabel + end + params do + requires :name, type: String, desc: 'The name of the label to be updated' + optional :new_name, type: String, desc: 'The new name of the label' + optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" + optional :description, type: String, desc: 'The new description of label' + at_least_one_of :new_name, :color, :description + end + put ':id/labels' do + update_label(user_group, Entities::GroupLabel) + end + + desc 'Delete an existing label' do + detail 'This feature was added in GitLab 11.8' + success Entities::GroupLabel + end + params do + requires :name, type: String, desc: 'The name of the label to be deleted' + end + delete ':id/labels' do + delete_label(user_group) + end + end + end +end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index e3d0b981065..2eb7b04711a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -84,8 +84,8 @@ module API page || not_found!('Wiki Page') end - def available_labels_for(label_parent) - search_params = { include_ancestor_groups: true } + def available_labels_for(label_parent, include_ancestor_groups: true) + search_params = { include_ancestor_groups: include_ancestor_groups } if label_parent.is_a?(Project) search_params[:project_id] = label_parent.id @@ -170,13 +170,6 @@ module API end end - def find_project_label(id) - labels = available_labels_for(user_project) - label = labels.find_by_id(id) || labels.find_by_title(id) - - label || not_found!('Label') - end - # rubocop: disable CodeReuse/ActiveRecord def find_project_issue(iid) IssuesFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) diff --git a/lib/api/helpers/label_helpers.rb b/lib/api/helpers/label_helpers.rb new file mode 100644 index 00000000000..c11e7d614ab --- /dev/null +++ b/lib/api/helpers/label_helpers.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module API + module Helpers + module LabelHelpers + extend Grape::API::Helpers + + params :label_create_params do + requires :name, type: String, desc: 'The name of the label to be created' + requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" + optional :description, type: String, desc: 'The description of label to be created' + end + + def find_label(parent, id, include_ancestor_groups: true) + labels = available_labels_for(parent, include_ancestor_groups: include_ancestor_groups) + label = labels.find_by_id(id) || labels.find_by_title(id) + + label || not_found!('Label') + end + + def get_labels(parent, entity) + present paginate(available_labels_for(parent)), with: entity, current_user: current_user, parent: parent + end + + def create_label(parent, entity) + authorize! :admin_label, parent + + label = available_labels_for(parent).find_by_title(params[:name]) + conflict!('Label already exists') if label + + priority = params.delete(:priority) + label_params = declared_params(include_missing: false) + + label = + if parent.is_a?(Project) + ::Labels::CreateService.new(label_params).execute(project: parent) + else + ::Labels::CreateService.new(label_params).execute(group: parent) + end + + if label.persisted? + if parent.is_a?(Project) + label.prioritize!(parent, priority) if priority + end + + present label, with: entity, current_user: current_user, parent: parent + else + render_validation_error!(label) + end + end + + def update_label(parent, entity) + authorize! :admin_label, parent + + label = find_label(parent, params[:name], include_ancestor_groups: false) + update_priority = params.key?(:priority) + priority = params.delete(:priority) + + label = ::Labels::UpdateService.new(declared_params(include_missing: false)).execute(label) + render_validation_error!(label) unless label.valid? + + if parent.is_a?(Project) && update_priority + if priority.nil? + label.unprioritize!(parent) + else + label.prioritize!(parent, priority) + end + end + + present label, with: entity, current_user: current_user, parent: parent + end + + def delete_label(parent) + authorize! :admin_label, parent + + label = find_label(parent, params[:name], include_ancestor_groups: false) + + destroy_conditionally!(label) + end + end + end +end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index d5eb2b94669..d729d3ee625 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -3,6 +3,7 @@ module API class Labels < Grape::API include PaginationParams + helpers ::API::Helpers::LabelHelpers before { authenticate! } @@ -11,62 +12,28 @@ module API end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get all labels of the project' do - success Entities::Label + success Entities::ProjectLabel end params do use :pagination end get ':id/labels' do - present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project + get_labels(user_project, Entities::ProjectLabel) end desc 'Create a new label' do - success Entities::Label + success Entities::ProjectLabel end params do - requires :name, type: String, desc: 'The name of the label to be created' - requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" - optional :description, type: String, desc: 'The description of label to be created' + use :label_create_params optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true end - # rubocop: disable CodeReuse/ActiveRecord post ':id/labels' do - authorize! :admin_label, user_project - - label = available_labels_for(user_project).find_by(title: params[:name]) - conflict!('Label already exists') if label - - priority = params.delete(:priority) - label = ::Labels::CreateService.new(declared_params(include_missing: false)).execute(project: user_project) - - if label.valid? - label.prioritize!(user_project, priority) if priority - present label, with: Entities::Label, current_user: current_user, project: user_project - else - render_validation_error!(label) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Delete an existing label' do - success Entities::Label - end - params do - requires :name, type: String, desc: 'The name of the label to be deleted' - end - # rubocop: disable CodeReuse/ActiveRecord - delete ':id/labels' do - authorize! :admin_label, user_project - - label = user_project.labels.find_by(title: params[:name]) - not_found!('Label') unless label - - destroy_conditionally!(label) + create_label(user_project, Entities::ProjectLabel) end - # rubocop: enable CodeReuse/ActiveRecord desc 'Update an existing label. At least one optional parameter is required.' do - success Entities::Label + success Entities::ProjectLabel end params do requires :name, type: String, desc: 'The name of the label to be updated' @@ -76,33 +43,19 @@ module API optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true at_least_one_of :new_name, :color, :description, :priority end - # rubocop: disable CodeReuse/ActiveRecord put ':id/labels' do - authorize! :admin_label, user_project - - label = user_project.labels.find_by(title: params[:name]) - not_found!('Label not found') unless label - - update_priority = params.key?(:priority) - priority = params.delete(:priority) - label_params = declared_params(include_missing: false) - # Rename new name to the actual label attribute name - label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name) - - label = ::Labels::UpdateService.new(label_params).execute(label) - render_validation_error!(label) unless label.valid? - - if update_priority - if priority.nil? - label.unprioritize!(user_project) - else - label.prioritize!(user_project, priority) - end - end + update_label(user_project, Entities::ProjectLabel) + end - present label, with: Entities::Label, current_user: current_user, project: user_project + desc 'Delete an existing label' do + success Entities::ProjectLabel + end + params do + requires :name, type: String, desc: 'The name of the label to be deleted' + end + delete ':id/labels' do + delete_label(user_project) end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4179aaa93a0..df46b4446ff 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -343,6 +343,7 @@ module API end params do optional :merge_commit_message, type: String, desc: 'Custom merge commit message' + optional :squash_commit_message, type: String, desc: 'Custom squash commit message' optional :should_remove_source_branch, type: Boolean, desc: 'When true, the source branch will be deleted if possible' optional :merge_when_pipeline_succeeds, type: Boolean, @@ -370,6 +371,7 @@ module API merge_params = { commit_message: params[:merge_commit_message], + squash_commit_message: params[:squash_commit_message], should_remove_source_branch: params[:should_remove_source_branch] } diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 95371961398..b16faffe335 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -121,6 +121,7 @@ module API optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' + optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index 74ad3c35a61..dfb54446ddf 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -2,51 +2,88 @@ module API class Subscriptions < Grape::API + helpers ::API::Helpers::LabelHelpers + before { authenticate! } - subscribable_types = { - 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) }, - 'issues' => proc { |id| find_project_issue(id) }, - 'labels' => proc { |id| find_project_label(id) } - } + subscribables = [ + { + type: 'merge_requests', + entity: Entities::MergeRequest, + source: Project, + finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) } + }, + { + type: 'issues', + entity: Entities::Issue, + source: Project, + finder: ->(id) { find_project_issue(id) } + }, + { + type: 'labels', + entity: Entities::ProjectLabel, + source: Project, + finder: ->(id) { find_label(user_project, id) } + }, + { + type: 'labels', + entity: Entities::GroupLabel, + source: Group, + finder: ->(id) { find_label(user_group, id) } + } + ] - params do - requires :id, type: String, desc: 'The ID of a project' - requires :subscribable_id, type: String, desc: 'The ID of a resource' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - subscribable_types.each do |type, finder| - type_singularized = type.singularize - entity_class = Entities.const_get(type_singularized.camelcase) + subscribables.each do |subscribable| + source_type = subscribable[:source].name.underscore + params do + requires :id, type: String, desc: "The #{source_type} ID" + requires :subscribable_id, type: String, desc: 'The ID of a resource' + end + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Subscribe to a resource' do - success entity_class + success subscribable[:entity] end - post ":id/#{type}/:subscribable_id/subscribe" do - resource = instance_exec(params[:subscribable_id], &finder) + post ":id/#{subscribable[:type]}/:subscribable_id/subscribe" do + parent = parent_resource(source_type) + resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) - if resource.subscribed?(current_user, user_project) + if resource.subscribed?(current_user, parent) not_modified! else - resource.subscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project + resource.subscribe(current_user, parent) + present resource, with: subscribable[:entity], current_user: current_user, project: parent, parent: parent end end desc 'Unsubscribe from a resource' do - success entity_class + success subscribable[:entity] end - post ":id/#{type}/:subscribable_id/unsubscribe" do - resource = instance_exec(params[:subscribable_id], &finder) + post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe" do + parent = parent_resource(source_type) + resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) - if !resource.subscribed?(current_user, user_project) + if !resource.subscribed?(current_user, parent) not_modified! else - resource.unsubscribe(current_user, user_project) - present resource, with: entity_class, current_user: current_user, project: user_project + resource.unsubscribe(current_user, parent) + present resource, with: subscribable[:entity], current_user: current_user, project: parent, parent: parent end end end end + + private + + helpers do + def parent_resource(source_type) + case source_type + when 'project' + user_project + else + nil + end + end + end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 184c7418e75..22ed1d8e7b4 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -93,7 +93,7 @@ module Backup progress.puts "Error: #{e}".color(:red) end else - restore_repo_success = gitlab_shell.create_repository(project.repository_storage, project.disk_path) + restore_repo_success = gitlab_shell.create_project_repository(project) end if restore_repo_success diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 7aa02009aa0..b2ef04d23d7 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -12,6 +12,9 @@ module Gitlab # Scopes used for OpenID Connect OPENID_SCOPES = [:openid].freeze + # OpenID Connect profile scopes + PROFILE_SCOPES = [:profile, :email].freeze + # Default scopes for OAuth applications that don't define their own DEFAULT_SCOPES = [:api].freeze @@ -284,7 +287,7 @@ module Gitlab # Other available scopes def optional_scopes - available_scopes + OPENID_SCOPES - DEFAULT_SCOPES + available_scopes + OPENID_SCOPES + PROFILE_SCOPES - DEFAULT_SCOPES end def registry_scopes diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index eaead41a720..75a3f17f549 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -65,9 +65,9 @@ module Gitlab def import_wiki return if project.wiki.repository_exists? - disk_path = project.wiki.disk_path - import_url = project.import_url.sub(/\.git\z/, ".git/wiki") - gitlab_shell.import_repository(project.repository_storage, disk_path, import_url) + wiki = WikiFormatter.new(project) + + gitlab_shell.import_wiki_repository(project, wiki) rescue StandardError => e errors << { type: :wiki, errors: e.message } end diff --git a/lib/gitlab/bitbucket_import/wiki_formatter.rb b/lib/gitlab/bitbucket_import/wiki_formatter.rb new file mode 100644 index 00000000000..b8ff43b777b --- /dev/null +++ b/lib/gitlab/bitbucket_import/wiki_formatter.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module BitbucketImport + class WikiFormatter + attr_reader :project + + def initialize(project) + @project = project + end + + def disk_path + project.wiki.disk_path + end + + def full_path + project.wiki.full_path + end + + def import_url + project.import_url.sub(/\.git\z/, ".git/wiki") + end + end + end +end diff --git a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml new file mode 100644 index 00000000000..9c534b2b8e7 --- /dev/null +++ b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml @@ -0,0 +1,121 @@ +# Read more about how to use this script on this blog post https://about.gitlab.com/2019/01/28/android-publishing-with-gitlab-and-fastlane/ +# You will also need to configure your build.gradle, Dockerfile, and fastlane configuration to make this work. +# If you are looking for a simpler template that does not publish, see the Android template. + +stages: + - environment + - build + - test + - internal + - alpha + - beta + - production + + +.updateContainerJob: + image: docker:stable + stage: environment + services: + - docker:dind + script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG || true + - docker build --cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG . + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + +updateContainer: + extends: .updateContainerJob + only: + changes: + - Dockerfile + +ensureContainer: + extends: .updateContainerJob + allow_failure: true + before_script: + - "mkdir -p ~/.docker && echo '{\"experimental\": \"enabled\"}' > ~/.docker/config.json" + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY + # Skip update container `script` if the container already exists + # via https://gitlab.com/gitlab-org/gitlab-ce/issues/26866#note_97609397 -> https://stackoverflow.com/a/52077071/796832 + - docker manifest inspect $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG > /dev/null && exit || true + + +.build_job: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + stage: build + before_script: + # We store this binary file in a variable as hex with this command: `xxd -p android-app.jks` + # Then we convert the hex back to a binary file + - echo "$signing_jks_file_hex" | xxd -r -p - > android-signing-keystore.jks + - "export VERSION_CODE=$CI_PIPELINE_IID && echo $VERSION_CODE" + - "export VERSION_SHA=`echo ${CI_COMMIT_SHA:0:8}` && echo $VERSION_SHA" + after_script: + - rm -f android-signing-keystore.jks || true + artifacts: + paths: + - app/build/outputs + +buildDebug: + extends: .build_job + script: + - bundle exec fastlane buildDebug + +buildRelease: + extends: .build_job + script: + - bundle exec fastlane buildRelease + environment: + name: production + +testDebug: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + stage: test + dependencies: + - buildDebug + script: + - bundle exec fastlane test + +publishInternal: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + stage: internal + dependencies: + - buildRelease + when: manual + before_script: + - echo $google_play_service_account_api_key_json > ~/google_play_api_key.json + after_script: + - rm ~/google_play_api_key.json + script: + - bundle exec fastlane internal + +.promote_job: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + when: manual + dependencies: [] + before_script: + - echo $google_play_service_account_api_key_json > ~/google_play_api_key.json + after_script: + - rm ~/google_play_api_key.json + +promoteAlpha: + extends: .promote_job + stage: alpha + script: + - bundle exec fastlane promote_internal_to_alpha + +promoteBeta: + extends: .promote_job + stage: beta + script: + - bundle exec fastlane promote_alpha_to_beta + +promoteProduction: + extends: .promote_job + stage: production + # We only allow production promotion on `master` because + # it has its own production scoped secret variables + only: + - master + script: + - bundle exec fastlane promote_beta_to_production +
\ No newline at end of file diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml index 6e138639b71..c169e3f7686 100644 --- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml @@ -1,4 +1,6 @@ # Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny +# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template. + image: openjdk:8-jdk variables: diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 75a5bf142d2..e369d26f22f 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -21,8 +21,8 @@ # # In order to deploy, you must have a Kubernetes cluster configured either # via a project integration, or via group/project variables. -# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project -# level, or manually added below. +# KUBE_INGRESS_BASE_DOMAIN must also be set on the cluster settings, +# as a variable at the group or project level, or manually added below. # # Continuous deployment to production is enabled by default. # If you want to deploy to staging first, set STAGING_ENABLED environment variable. @@ -41,8 +41,8 @@ image: alpine:latest variables: - # AUTO_DEVOPS_DOMAIN is the application deployment domain and should be set as a variable at the group or project level. - # AUTO_DEVOPS_DOMAIN: domain.example.com + # KUBE_INGRESS_BASE_DOMAIN is the application deployment domain and should be set as a variable at the group or project level. + # KUBE_INGRESS_BASE_DOMAIN: domain.example.com POSTGRES_USER: user POSTGRES_PASSWORD: testing-password @@ -251,7 +251,7 @@ review: - persist_environment_url environment: name: review/$CI_COMMIT_REF_NAME - url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN + url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN on_stop: stop_review artifacts: paths: [environment_url.txt] @@ -306,7 +306,7 @@ staging: - deploy environment: name: staging - url: http://$CI_PROJECT_PATH_SLUG-staging.$AUTO_DEVOPS_DOMAIN + url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN only: refs: - master @@ -330,7 +330,7 @@ canary: - deploy canary environment: name: production - url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN when: manual only: refs: @@ -354,7 +354,7 @@ canary: - persist_environment_url environment: name: production - url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN artifacts: paths: [environment_url.txt] @@ -403,7 +403,7 @@ production_manual: - persist_environment_url environment: name: production - url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN + url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN artifacts: paths: [environment_url.txt] @@ -689,7 +689,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \ + --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ --set service.url="$CI_ENVIRONMENT_URL" \ --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ @@ -725,7 +725,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ - --set service.commonName="le.$AUTO_DEVOPS_DOMAIN" \ + --set service.commonName="le.$KUBE_INGRESS_BASE_DOMAIN" \ --set service.url="$CI_ENVIRONMENT_URL" \ --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ @@ -823,11 +823,24 @@ rollout 100%: kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE" } + + # Function to ensure backwards compatibility with AUTO_DEVOPS_DOMAIN + function ensure_kube_ingress_base_domain() { + if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then + export KUBE_INGRESS_BASE_DOMAIN=$AUTO_DEVOPS_DOMAIN + fi + } + function check_kube_domain() { - if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then - echo "In order to deploy or use Review Apps, AUTO_DEVOPS_DOMAIN variable must be set" - echo "You can do it in Auto DevOps project settings or defining a variable at group or project level" + ensure_kube_ingress_base_domain + + if [ -z ${KUBE_INGRESS_BASE_DOMAIN+x} ]; then + echo "In order to deploy or use Review Apps," + echo "AUTO_DEVOPS_DOMAIN or KUBE_INGRESS_BASE_DOMAIN variables must be set" + echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings" + echo "or by defining a variable at group or project level." echo "You can also manually add it in .gitlab-ci.yml" + echo "AUTO_DEVOPS_DOMAIN support will be dropped on 12.0" false else true diff --git a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml index fc3d4ecdbba..25a32ba0f74 100644 --- a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml @@ -57,6 +57,7 @@ test_job: script: - '& "$env:NUNIT_PATH" ".\$env:TEST_FOLDER\Tests.dll"' # running NUnit tests artifacts: + when: always # save test results even when the task fails expire_in: 1 week # save gitlab server space, we copy the files we need to deploy folder later on paths: - '.\TestResult.xml' # saving NUnit results to copy to deploy folder diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb index 1c6242b444a..e93ca3e11f8 100644 --- a/lib/gitlab/git/object_pool.rb +++ b/lib/gitlab/git/object_pool.rb @@ -10,12 +10,13 @@ module Gitlab delegate :exists?, :size, to: :repository delegate :unlink_repository, :delete, to: :object_pool_service - attr_reader :storage, :relative_path, :source_repository + attr_reader :storage, :relative_path, :source_repository, :gl_project_path - def initialize(storage, relative_path, source_repository) + def initialize(storage, relative_path, source_repository, gl_project_path) @storage = storage @relative_path = relative_path @source_repository = source_repository + @gl_project_path = gl_project_path end def create @@ -31,12 +32,12 @@ module Gitlab end def to_gitaly_repository - Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY) + Gitlab::GitalyClient::Util.repository(storage, relative_path, GL_REPOSITORY, gl_project_path) end # Allows for reusing other RPCs by 'tricking' Gitaly to think its a repository def repository - @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY) + @repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path) end private diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 786c90f9272..54bbd531398 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -67,7 +67,7 @@ module Gitlab # Relative path of repo attr_reader :relative_path - attr_reader :storage, :gl_repository, :relative_path + attr_reader :storage, :gl_repository, :relative_path, :gl_project_path # This remote name has to be stable for all types of repositories that # can join an object pool. If it's structure ever changes, a migration @@ -78,10 +78,11 @@ module Gitlab # This initializer method is only used on the client side (gitlab-ce). # Gitaly-ruby uses a different initializer. - def initialize(storage, relative_path, gl_repository) + def initialize(storage, relative_path, gl_repository, gl_project_path) @storage = storage @relative_path = relative_path @gl_repository = gl_repository + @gl_project_path = gl_project_path @name = @relative_path.split("/").last end @@ -872,7 +873,7 @@ module Gitlab end def gitaly_repository - Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository) + Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path) end def gitaly_ref_client diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 8a1abfbf874..a7e20d9429e 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -324,13 +324,40 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files) end - def search_files_by_content(ref, query) - request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) - GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches) + def search_files_by_content(ref, query, chunked_response: true) + request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query, chunked_response: chunked_response) + response = GitalyClient.call(@storage, :repository_service, :search_files_by_content, request) + + search_results_from_response(response) end private + def search_results_from_response(gitaly_response) + matches = [] + current_match = +"" + + gitaly_response.each do |message| + next if message.nil? + + # Old client will ignore :chunked_response flag + # and return messages with `matches` key. + # This code path will be removed post 12.0 release + if message.matches.any? + matches += message.matches + else + current_match << message.match_data + + if message.end_of_match + matches << current_match + current_match = +"" + end + end + end + + matches + end + def gitaly_fetch_stream_to_file(save_path, rpc_name, request_class, timeout) request = request_class.new(repository: @gitaly_repo) response = GitalyClient.call( diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index dce5d6a8ad0..899921f76e4 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -4,7 +4,7 @@ module Gitlab module GitalyClient module Util class << self - def repository(repository_storage, relative_path, gl_repository) + def repository(repository_storage, relative_path, gl_repository, gl_project_path) git_env = Gitlab::Git::HookEnv.all(gl_repository) git_object_directory = git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence git_alternate_object_directories = Array.wrap(git_env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE']) @@ -14,14 +14,16 @@ module Gitlab relative_path: relative_path, gl_repository: gl_repository.to_s, git_object_directory: git_object_directory.to_s, - git_alternate_object_directories: git_alternate_object_directories + git_alternate_object_directories: git_alternate_object_directories, + gl_project_path: gl_project_path ) end def git_repository(gitaly_repository) Gitlab::Git::Repository.new(gitaly_repository.storage_name, gitaly_repository.relative_path, - gitaly_repository.gl_repository) + gitaly_repository.gl_repository, + gitaly_repository.gl_project_path) end end end diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb index bc3ea9e9226..e2dfb00dcc5 100644 --- a/lib/gitlab/github_import/importer/repository_importer.rb +++ b/lib/gitlab/github_import/importer/repository_importer.rb @@ -6,11 +6,12 @@ module Gitlab class RepositoryImporter include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :project, :client, :wiki_formatter def initialize(project, client) @project = project @client = client + @wiki_formatter = ::Gitlab::LegacyGithubImport::WikiFormatter.new(project) end # Returns true if we should import the wiki for the project. @@ -57,9 +58,7 @@ module Gitlab end def import_wiki_repository - wiki_path = "#{project.disk_path}.wiki" - - gitlab_shell.import_repository(project.repository_storage, wiki_path, wiki_url) + gitlab_shell.import_wiki_repository(project, wiki_formatter) true rescue Gitlab::Shell::Error => e @@ -72,7 +71,7 @@ module Gitlab end def wiki_url - project.import_url.sub(/\.git\z/, '.wiki.git') + wiki_formatter.import_url end def update_clone_time diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 9b1794eec91..3235d3ccc4e 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -24,6 +24,7 @@ module Gitlab gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites') gon.test_env = Rails.env.test? gon.suggested_label_colors = LabelsHelper.suggested_colors + gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index b9903e37f40..7dfd9ed4f35 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -20,14 +20,7 @@ module Gitlab kubeclient.create_pod(command.pod_resource) end - def update(command) - namespace.ensure_exists! - - update_config_map(command) - - delete_pod!(command.pod_name) - kubeclient.create_pod(command.pod_resource) - end + alias_method :update, :install ## # Returns Pod phase @@ -62,6 +55,8 @@ module Gitlab def create_config_map(command) command.config_map_resource.tap do |config_map_resource| + break unless config_map_resource + if config_map_exists?(config_map_resource) kubeclient.update_config_map(config_map_resource) else diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index a1ab5e048ac..f931248b747 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -42,8 +42,17 @@ module Gitlab 'helm repo update' if repository end + # Uses `helm upgrade --install` which means we can use this for both + # installation and uprade of applications def install_command - command = ['helm', 'install', chart] + install_command_flags + command = ['helm', 'upgrade', name, chart] + + install_flag + + reset_values_flag + + optional_tls_flags + + optional_version_flag + + rbac_create_flag + + namespace_flag + + value_flag command.shelljoin end @@ -56,17 +65,20 @@ module Gitlab postinstall.join("\n") if postinstall end - def install_command_flags - name_flag = ['--name', name] - namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE] - value_flag = ['-f', "/data/helm/#{name}/config/values.yaml"] + def install_flag + ['--install'] + end - name_flag + - optional_tls_flags + - optional_version_flag + - rbac_create_flag + - namespace_flag + - value_flag + def reset_values_flag + ['--reset-values'] + end + + def value_flag + ['-f', "/data/helm/#{name}/config/values.yaml"] + end + + def namespace_flag + ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE] end def rbac_create_flag diff --git a/lib/gitlab/kubernetes/helm/upgrade_command.rb b/lib/gitlab/kubernetes/helm/upgrade_command.rb deleted file mode 100644 index 9daffc138b5..00000000000 --- a/lib/gitlab/kubernetes/helm/upgrade_command.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Kubernetes - module Helm - class UpgradeCommand - include BaseCommand - include ClientCommand - - attr_reader :name, :chart, :version, :repository, :files - - def initialize(name, chart:, files:, rbac:, version: nil, repository: nil) - @name = name - @chart = chart - @rbac = rbac - @version = version - @files = files - @repository = repository - end - - def generate_script - super + [ - init_command, - wait_for_tiller_command, - repository_command, - script_command - ].compact.join("\n") - end - - def rbac? - @rbac - end - - def pod_name - "upgrade-#{name}" - end - - private - - def script_command - upgrade_flags = "#{optional_version_flag}#{optional_tls_flags}" \ - " --reset-values" \ - " --install" \ - " --namespace #{::Gitlab::Kubernetes::Helm::NAMESPACE}" \ - " -f /data/helm/#{name}/config/values.yaml" - - "helm upgrade #{name} #{chart}#{upgrade_flags}" - end - - def optional_version_flag - " --version #{version}" if version - end - - def optional_tls_flags - return unless files.key?(:'ca.pem') - - " --tls" \ - " --tls-ca-cert #{files_dir}/ca.pem" \ - " --tls-cert #{files_dir}/cert.pem" \ - " --tls-key #{files_dir}/key.pem" - end - end - end - end -end diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb index c526d31a591..f3323c98af2 100644 --- a/lib/gitlab/legacy_github_import/importer.rb +++ b/lib/gitlab/legacy_github_import/importer.rb @@ -267,7 +267,7 @@ module Gitlab def import_wiki unless project.wiki.repository_exists? wiki = WikiFormatter.new(project) - gitlab_shell.import_repository(project.repository_storage, wiki.disk_path, wiki.import_url) + gitlab_shell.import_wiki_repository(project, wiki) end rescue Gitlab::Shell::Error => e # GitHub error message when the wiki repo has not been created, diff --git a/lib/gitlab/legacy_github_import/wiki_formatter.rb b/lib/gitlab/legacy_github_import/wiki_formatter.rb index ea52be5ee0f..cf1e21ad1e1 100644 --- a/lib/gitlab/legacy_github_import/wiki_formatter.rb +++ b/lib/gitlab/legacy_github_import/wiki_formatter.rb @@ -13,6 +13,10 @@ module Gitlab project.wiki.disk_path end + def full_path + project.wiki.full_path + end + def import_url project.import_url.sub(/\.git\z/, ".wiki.git") end diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb index 4c4ec026823..4c5b849cc51 100644 --- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb +++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb @@ -23,13 +23,13 @@ module Gitlab def sample Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats| - unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active) - unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued) + unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active) + unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued) end Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats| - unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active) - unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued) + unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active) + unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued) end end diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index 3bfd6ee892c..ef656e5b2ce 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -2,14 +2,12 @@ module Gitlab class ProjectTemplate - attr_reader :title, :name, :description, :preview + attr_reader :title, :name, :description, :preview, :logo - def initialize(name, title, description, preview) - @name, @title, @description, @preview = name, title, description, preview + def initialize(name, title, description, preview, logo = 'illustrations/gitlab_logo.svg') + @name, @title, @description, @preview, @logo = name, title, description, preview, logo end - alias_method :logo, :name - def file archive_path.open end @@ -27,9 +25,14 @@ module Gitlab end TEMPLATES_TABLE = [ - ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'), - ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw and pom.xml to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'), - ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express') + ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), + ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), + ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), + ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo'), + ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll'), + ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), + ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook'), + ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo') ].freeze class << self diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index bdf21cf3134..1153e69d3de 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -64,27 +64,48 @@ module Gitlab end end + # Convenience methods for initializing a new repository with a Project model. + def create_project_repository(project) + create_repository(project.repository_storage, project.disk_path, project.full_path) + end + + def create_wiki_repository(project) + create_repository(project.repository_storage, project.wiki.disk_path, project.wiki.full_path) + end + # Init new repository # # storage - the shard key - # name - project disk path + # disk_path - project disk path + # gl_project_path - project name # # Ex. - # create_repository("default", "gitlab/gitlab-ci") + # create_repository("default", "path/to/gitlab-ci", "gitlab/gitlab-ci") # - def create_repository(storage, name) - relative_path = name.dup + def create_repository(storage, disk_path, gl_project_path) + relative_path = disk_path.dup relative_path << '.git' unless relative_path.end_with?('.git') - repository = Gitlab::Git::Repository.new(storage, relative_path, '') + # During creation of a repository, gl_repository may not be known + # because that depends on a yet-to-be assigned project ID in the + # database (e.g. project-1234), so for now it is blank. + repository = Gitlab::Git::Repository.new(storage, relative_path, '', gl_project_path) wrapped_gitaly_errors { repository.gitaly_repository_client.create_repository } true rescue => err # Once the Rugged codes gets removes this can be improved - Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}") + Rails.logger.error("Failed to add repository #{storage}/#{disk_path}: #{err}") false end + def import_wiki_repository(project, wiki_formatter) + import_repository(project.repository_storage, wiki_formatter.disk_path, wiki_formatter.import_url, project.wiki.full_path) + end + + def import_project_repository(project) + import_repository(project.repository_storage, project.disk_path, project.import_url, project.full_path) + end + # Import repository # # storage - project's storage name @@ -94,13 +115,13 @@ module Gitlab # Ex. # import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # - def import_repository(storage, name, url) + def import_repository(storage, name, url, gl_project_path) if url.start_with?('.', '/') raise Error.new("don't use disk paths with import_repository: #{url.inspect}") end relative_path = "#{name}.git" - cmd = GitalyGitlabProjects.new(storage, relative_path) + cmd = GitalyGitlabProjects.new(storage, relative_path, gl_project_path) success = cmd.import_project(url, git_timeout) raise Error, cmd.output unless success @@ -125,18 +146,13 @@ module Gitlab end # Fork repository to new path - # forked_from_storage - forked-from project's storage name - # forked_from_disk_path - project disk relative path - # forked_to_storage - forked-to project's storage name - # forked_to_disk_path - forked project disk relative path - # - # Ex. - # fork_repository("nfs-file06", "gitlab/gitlab-ci", "nfs-file07", "new-namespace/gitlab-ci") - def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) - forked_from_relative_path = "#{forked_from_disk_path}.git" - fork_args = [forked_to_storage, "#{forked_to_disk_path}.git"] + # source_project - forked-from Project + # target_project - forked-to Project + def fork_repository(source_project, target_project) + forked_from_relative_path = "#{source_project.disk_path}.git" + fork_args = [target_project.repository_storage, "#{target_project.disk_path}.git", target_project.full_path] - GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args) + GitalyGitlabProjects.new(source_project.repository_storage, forked_from_relative_path, source_project.full_path).fork_repository(*fork_args) end # Removes a repository from file system, using rm_diretory which is an alias @@ -397,16 +413,17 @@ module Gitlab end class GitalyGitlabProjects - attr_reader :shard_name, :repository_relative_path, :output + attr_reader :shard_name, :repository_relative_path, :output, :gl_project_path - def initialize(shard_name, repository_relative_path) + def initialize(shard_name, repository_relative_path, gl_project_path) @shard_name = shard_name @repository_relative_path = repository_relative_path @output = '' + @gl_project_path = gl_project_path end def import_project(source, _timeout) - raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) + raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil, gl_project_path) Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source) true @@ -415,9 +432,9 @@ module Gitlab false end - def fork_repository(new_shard_name, new_repository_relative_path) - target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil) - raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) + def fork_repository(new_shard_name, new_repository_relative_path, new_project_name) + target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil, new_project_name) + raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil, gl_project_path) Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository) rescue GRPC::BadStatus => e diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9ec590f90d8..a3f78968a55 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22,16 +22,6 @@ msgstr "" msgid " or " msgstr "" -msgid "%d addition" -msgid_plural "%d additions" -msgstr[0] "" -msgstr[1] "" - -msgid "%d changed file" -msgid_plural "%d changed files" -msgstr[0] "" -msgstr[1] "" - msgid "%d commit" msgid_plural "%d commits" msgstr[0] "" @@ -42,10 +32,8 @@ msgid_plural "%d commits behind" msgstr[0] "" msgstr[1] "" -msgid "%d deleted" -msgid_plural "%d deletions" -msgstr[0] "" -msgstr[1] "" +msgid "%d commits" +msgstr "" msgid "%d exporter" msgid_plural "%d exporters" @@ -138,9 +126,6 @@ msgstr "" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgstr "" -msgid "%{nip_domain} can be used as an alternative to a custom domain." -msgstr "" - msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgstr "" @@ -900,15 +885,6 @@ msgstr "" msgid "Auto DevOps, runners and job artifacts" msgstr "" -msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly." -msgstr "" - -msgid "Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly." -msgstr "" - -msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly." -msgstr "" - msgid "Auto-cancel redundant, pending pipelines" msgstr "" @@ -1281,9 +1257,6 @@ msgstr "" msgid "CICD|Deployment strategy" msgstr "" -msgid "CICD|Deployment strategy needs a domain name to work correctly." -msgstr "" - msgid "CICD|Jobs" msgstr "" @@ -1293,7 +1266,7 @@ msgstr "" msgid "CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found." msgstr "" -msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages." +msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly." msgstr "" msgid "CICD|instance enabled" @@ -1566,6 +1539,12 @@ msgstr "" msgid "Closed (moved)" msgstr "" +msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." +msgstr "" + +msgid "ClusterIntegration| can be used instead of a custom domain." +msgstr "" + msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}" msgstr "" @@ -1575,6 +1554,9 @@ msgstr "" msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." msgstr "" +msgid "ClusterIntegration|%{title} upgraded successfully." +msgstr "" + msgid "ClusterIntegration|API URL" msgstr "" @@ -1596,6 +1578,9 @@ msgstr "" msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}" msgstr "" +msgid "ClusterIntegration|Alternatively" +msgstr "" + msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}" msgstr "" @@ -1617,6 +1602,9 @@ msgstr "" msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster." msgstr "" +msgid "ClusterIntegration|Base domain" +msgstr "" + msgid "ClusterIntegration|CA Certificate" msgstr "" @@ -1893,6 +1881,9 @@ msgstr "" msgid "ClusterIntegration|Request to begin installing failed" msgstr "" +msgid "ClusterIntegration|Retry upgrade" +msgstr "" + msgid "ClusterIntegration|Save changes" msgstr "" @@ -1935,12 +1926,18 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" +msgid "ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again." +msgstr "" + msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Something went wrong while installing %{title}" msgstr "" +msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain." +msgstr "" + msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." msgstr "" @@ -1956,6 +1953,18 @@ msgstr "" msgid "ClusterIntegration|Token" msgstr "" +msgid "ClusterIntegration|Upgrade" +msgstr "" + +msgid "ClusterIntegration|Upgrade failed" +msgstr "" + +msgid "ClusterIntegration|Upgraded" +msgstr "" + +msgid "ClusterIntegration|Upgrading" +msgstr "" + msgid "ClusterIntegration|Validating project billing status" msgstr "" @@ -2444,6 +2453,9 @@ msgstr "" msgid "Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import." msgstr "" +msgid "Customize language and region related settings." +msgstr "" + msgid "Customize your pipeline configuration, view your pipeline status and coverage report." msgstr "" @@ -2507,6 +2519,12 @@ msgstr "" msgid "Default Branch" msgstr "" +msgid "Default first day of the week" +msgstr "" + +msgid "Default first day of the week in calendars and date pickers." +msgstr "" + msgid "Default: Directly import the Google Code email address or username" msgstr "" @@ -2546,6 +2564,9 @@ msgstr "" msgid "Delete list" msgstr "" +msgid "Delete source branch" +msgstr "" + msgid "Delete this attachment" msgstr "" @@ -3172,6 +3193,21 @@ msgstr "" msgid "Everyone can contribute" msgstr "" +msgid "Everything you need to create a GitLab Pages site using GitBook." +msgstr "" + +msgid "Everything you need to create a GitLab Pages site using Hexo." +msgstr "" + +msgid "Everything you need to create a GitLab Pages site using Hugo." +msgstr "" + +msgid "Everything you need to create a GitLab Pages site using Jekyll." +msgstr "" + +msgid "Everything you need to create a GitLab Pages site using plain HTML." +msgstr "" + msgid "Except policy:" msgstr "" @@ -3271,6 +3307,9 @@ msgstr "" msgid "Failure" msgstr "" +msgid "Fast-forward merge without a merge commit" +msgstr "" + msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)" msgstr "" @@ -3283,6 +3322,11 @@ msgstr "" msgid "Fields on this page are now uneditable, you can configure" msgstr "" +msgid "File" +msgid_plural "Files" +msgstr[0] "" +msgstr[1] "" + msgid "File added" msgstr "" @@ -3355,6 +3399,9 @@ msgstr "" msgid "Finished" msgstr "" +msgid "First day of the week" +msgstr "" + msgid "FirstPushedBy|First" msgstr "" @@ -3915,15 +3962,30 @@ msgstr "" msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgstr "" +msgid "Include merge request description" +msgstr "" + msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>." msgstr "" +msgid "Includes an MVC structure to help you get started." +msgstr "" + +msgid "Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started." +msgstr "" + +msgid "Includes an MVC structure, mvnw and pom.xml to help you get started." +msgstr "" + msgid "Incompatible Project" msgstr "" msgid "Indicates whether this runner can pick jobs without tags" msgstr "" +msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}." +msgstr "" + msgid "Inline" msgstr "" @@ -4297,6 +4359,9 @@ msgstr "" msgid "Loading…" msgstr "" +msgid "Localization" +msgstr "" + msgid "Lock" msgstr "" @@ -4447,9 +4512,18 @@ msgstr "" msgid "Merge Requests" msgstr "" +msgid "Merge commit message" +msgstr "" + msgid "Merge events" msgstr "" +msgid "Merge immediately" +msgstr "" + +msgid "Merge in progress" +msgstr "" + msgid "Merge request" msgstr "" @@ -4459,6 +4533,9 @@ msgstr "" msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgstr "" +msgid "Merge when pipeline succeeds" +msgstr "" + msgid "MergeRequests|Add a reply" msgstr "" @@ -4615,6 +4692,15 @@ msgstr "" msgid "Modal|Close" msgstr "" +msgid "Modify commit messages" +msgstr "" + +msgid "Modify merge commit" +msgstr "" + +msgid "Monday" +msgstr "" + msgid "Monitor your errors by integrating with Sentry" msgstr "" @@ -5087,6 +5173,9 @@ msgstr "" msgid "Pages Domains" msgstr "" +msgid "Pages getting started guide" +msgstr "" + msgid "Pagination|Last »" msgstr "" @@ -6605,7 +6694,10 @@ msgstr "" msgid "Snippets" msgstr "" -msgid "Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again." +msgid "Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again." +msgstr "" + +msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes." msgstr "" msgid "Something went wrong on our end" @@ -6632,6 +6724,9 @@ msgstr "" msgid "Something went wrong while closing the %{issuable}. Please try again later" msgstr "" +msgid "Something went wrong while deleting the source branch. Please try again." +msgstr "" + msgid "Something went wrong while fetching comments. Please try again." msgstr "" @@ -6644,6 +6739,9 @@ msgstr "" msgid "Something went wrong while fetching the registry list." msgstr "" +msgid "Something went wrong while merging this merge request. Please try again." +msgstr "" + msgid "Something went wrong while reopening the %{issuable}. Please try again later" msgstr "" @@ -6788,6 +6886,9 @@ msgstr "" msgid "Specify the following URL during the Runner setup:" msgstr "" +msgid "Squash commit message" +msgstr "" + msgid "Squash commits" msgstr "" @@ -6926,6 +7027,9 @@ msgstr "" msgid "Suggested change" msgstr "" +msgid "Sunday" +msgstr "" + msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it." msgstr "" @@ -6938,6 +7042,9 @@ msgstr "" msgid "System Info" msgstr "" +msgid "System default (%{default})" +msgstr "" + msgid "System metrics (Custom)" msgstr "" @@ -7977,6 +8084,9 @@ msgstr "" msgid "Various email settings." msgstr "" +msgid "Various localization settings." +msgstr "" + msgid "Various settings that affect GitLab performance." msgstr "" @@ -8304,6 +8414,9 @@ msgstr "" msgid "You can only edit files when you are on a branch" msgstr "" +msgid "You can only merge once the items above are resolved" +msgstr "" + msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" @@ -8598,6 +8711,12 @@ msgstr[1] "" msgid "missing" msgstr "" +msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}." +msgstr "" + +msgid "mrWidgetCommitsAdded|1 merge commit" +msgstr "" + msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgstr "" diff --git a/package.json b/package.json index 97d8fd3b17f..7110baef8b3 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@babel/plugin-syntax-import-meta": "^7.2.0", "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", - "@gitlab/svgs": "^1.48.0", + "@gitlab/svgs": "^1.51.0", "@gitlab/ui": "^2.0.2", "apollo-boost": "^0.1.20", "apollo-client": "^2.4.5", diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index d5377f1d1c1..e476cbe29a2 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -40,17 +40,9 @@ module QA element :login_page end - def initialize - # The login page is usually the entry point for all the scenarios so - # we need to wait for the instance to start. That said, in some cases - # we are already logged-in so we check both cases here. - # Check if we're already logged in first. If we don't then we have to - # wait 10 seconds for the check for the login page to fail every time - # we use this class when we're already logged in (E.g., whenever we - # create a personal access token to use for API access). - wait(max: 500) do - Page::Main::Menu.act { has_personal_area?(wait: 0) } || - has_element?(:login_page) + def page_loaded? + wait(max: 60) do + has_element?(:login_page) end end diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index 12c2409a5a7..2de39b8ebf5 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -13,9 +13,7 @@ module QA # rubocop:disable Naming/FileName view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do element :enable_auto_devops_field, 'check_box :enabled' # rubocop:disable QA/ElementWithPattern - element :domain_field, 'text_field :domain' # rubocop:disable QA/ElementWithPattern element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')" # rubocop:disable QA/ElementWithPattern - element :domain_input, "%strong= _('Domain')" # rubocop:disable QA/ElementWithPattern element :save_changes_button, "submit _('Save changes')" # rubocop:disable QA/ElementWithPattern end @@ -31,10 +29,9 @@ module QA # rubocop:disable Naming/FileName end end - def enable_auto_devops_with_domain(domain) + def enable_auto_devops expand_section(:autodevops_settings) do check 'Default to Auto DevOps pipeline' - fill_in 'Domain', with: domain click_on 'Save changes' end end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb index d67e5f6da20..986b31da528 100644 --- a/qa/qa/resource/kubernetes_cluster.rb +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -6,12 +6,16 @@ module QA module Resource class KubernetesCluster < Base attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain attribute :ingress_ip do Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) end + attribute :domain do + "#{ingress_ip}.nip.io" + end + def fabricate! @project.visit! diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index 6632c2977ef..2fb8402edd8 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Manage', :smoke do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/72 + context 'Manage', :smoke, :quarantine do describe 'Project creation' do it 'user creates a new project' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index d4cedc9362d..e172206eb88 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Manage' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/21 + context 'Manage', :quarantine do describe 'Project activity' do it 'user creates an event in the activity page upon Git push' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 10cc0480794..545da0a8b85 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/31 + context 'Create', :quarantine do describe 'Merge request squashing' do it 'user squashes commits while merging' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 0837b720df1..e444bc7ef1b 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Verify' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/30 + context 'Verify', :quarantine do describe 'CI variable support' do it 'user adds a CI variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 6f39a755392..aa01e5a618e 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Release' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/26 + context 'Release', :quarantine do describe 'Deploy key creation' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 11a9653db81..97d36095a69 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,7 +3,8 @@ require 'digest/sha1' module QA - context 'Release', :docker do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/70 + context 'Release', :docker, :quarantine do describe 'Git clone using a deploy key' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index b0ff83db86b..5c8ec465143 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -52,13 +52,13 @@ module QA end kubernetes_cluster.populate(:ingress_ip) - @project.visit! Page::Project::Menu.act { click_ci_cd_settings } Page::Project::Settings::CICD.perform do |p| - p.enable_auto_devops_with_domain( - "#{kubernetes_cluster.ingress_ip}.nip.io") + p.enable_auto_devops end + + kubernetes_cluster.populate(:domain) end after(:all) do diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 3537ba7c235..0f3cf5f4408 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -3,6 +3,20 @@ require_relative '../qa' Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } RSpec.configure do |config| + ServerNotRespondingError = Class.new(RuntimeError) + + # The login page could take some time to load the first time it is visited. + # We visit the login page and wait for it to properly load only once at the beginning of the suite. + config.before(:suite) do + if QA::Runtime::Scenario.respond_to?(:gitlab_address) + QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) + + unless QA::Page::Main::Login.perform(&:page_loaded?) + raise ServerNotRespondingError, "Login page did not load at #{QA::Page::Main::Login.perform(&:current_url)}" + end + end + end + config.before do |example| QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug? diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 0f28499194e..360030102e0 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -429,12 +429,14 @@ describe Groups::ClustersController do end let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) } + let(:domain) { 'test-domain.com' } let(:params) do { cluster: { enabled: false, - name: 'my-new-cluster-name' + name: 'my-new-cluster-name', + base_domain: domain } } end @@ -447,6 +449,20 @@ describe Groups::ClustersController do expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.') expect(cluster.enabled).to be_falsey expect(cluster.name).to eq('my-new-cluster-name') + expect(cluster.domain).to eq('test-domain.com') + end + + context 'when domain is invalid' do + let(:domain) { 'not-a-valid-domain' } + + it 'should not update cluster attributes' do + go + + cluster.reload + expect(response).to render_template(:show) + expect(cluster.name).not_to eq('my-new-cluster-name') + expect(cluster.domain).not_to eq('test-domain.com') + end end context 'when format is json' do @@ -456,7 +472,8 @@ describe Groups::ClustersController do { cluster: { enabled: false, - name: 'my-new-cluster-name' + name: 'my-new-cluster-name', + domain: domain } } end diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index 012f016b091..760c0fab130 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -42,7 +42,8 @@ describe Profiles::PreferencesController do prefs = { color_scheme_id: '1', dashboard: 'stars', - theme_id: '2' + theme_id: '2', + first_day_of_week: '1' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!) diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb index 6464398cea1..844c61f1ace 100644 --- a/spec/controllers/projects/error_tracking_controller_spec.rb +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -107,8 +107,11 @@ describe Projects::ErrorTrackingController do let(:http_status) { :no_content } before do - expect(list_issues_service).to receive(:execute) - .and_return(status: :error, message: error_message, http_status: http_status) + expect(list_issues_service).to receive(:execute).and_return( + status: :error, + message: error_message, + http_status: http_status + ) end it 'returns http_status with message' do @@ -122,6 +125,113 @@ describe Projects::ErrorTrackingController do end end + describe 'POST #list_projects' do + context 'with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'returns 404' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with an anonymous user' do + before do + sign_out(user) + end + + it 'redirects to sign-in page' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with authorized user' do + let(:list_projects_service) { spy(:list_projects_service) } + let(:sentry_project) { build(:error_tracking_project) } + + let(:permitted_params) do + ActionController::Parameters.new( + list_projects_params[:error_tracking_setting] + ).permit! + end + + before do + allow(ErrorTracking::ListProjectsService) + .to receive(:new).with(project, user, permitted_params) + .and_return(list_projects_service) + end + + context 'service result is successful' do + before do + expect(list_projects_service).to receive(:execute) + .and_return(status: :success, projects: [sentry_project]) + end + + it 'returns a list of projects' do + post :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/list_projects') + expect(json_response['projects']).to eq([sentry_project].as_json) + end + end + + context 'service result is erroneous' do + let(:error_message) { 'error message' } + + context 'without http_status' do + before do + expect(list_projects_service).to receive(:execute) + .and_return(status: :error, message: error_message) + end + + it 'returns 400 with message' do + get :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(error_message) + end + end + + context 'with explicit http_status' do + let(:http_status) { :no_content } + + before do + expect(list_projects_service).to receive(:execute).and_return( + status: :error, + message: error_message, + http_status: http_status + ) + end + + it 'returns http_status with message' do + get :list_projects, params: list_projects_params + + expect(response).to have_gitlab_http_status(http_status) + expect(json_response['message']).to eq(error_message) + end + end + end + end + + private + + def list_projects_params(opts = {}) + project_params( + format: :json, + error_tracking_setting: { + api_host: 'gitlab.com', + token: 'token' + } + ) + end + end + private def project_params(opts = {}) diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb new file mode 100644 index 00000000000..0a9c4bcaf12 --- /dev/null +++ b/spec/features/clusters/cluster_detail_page_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Clusterable > Show page' do + let(:current_user) { create(:user) } + + before do + sign_in(current_user) + end + + shared_examples 'editing domain' do + before do + clusterable.add_maintainer(current_user) + end + + it 'allow the user to set domain' do + visit cluster_path + + within '#cluster-integration' do + fill_in('cluster_base_domain', with: 'test.com') + click_on 'Save changes' + end + + expect(page.status_code).to eq(200) + expect(page).to have_content('Kubernetes cluster was successfully updated.') + end + + context 'when there is a cluster with ingress and external ip' do + before do + cluster.create_application_ingress!(external_ip: '192.168.1.100') + + visit cluster_path + end + + it 'shows help text with the domain as an alternative to custom domain' do + within '#cluster-integration' do + expect(page).to have_content('Alternatively 192.168.1.100.nip.io can be used instead of a custom domain') + end + end + end + + context 'when there is no ingress' do + it 'alternative to custom domain is not shown' do + visit cluster_path + + within '#cluster-integration' do + expect(page).not_to have_content('can be used instead of a custom domain.') + end + end + end + end + + context 'when clusterable is a project' do + it_behaves_like 'editing domain' do + let(:clusterable) { create(:project) } + let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } + let(:cluster_path) { project_cluster_path(clusterable, cluster) } + end + end + + context 'when clusterable is a group' do + it_behaves_like 'editing domain' do + let(:clusterable) { create(:group) } + let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) } + let(:cluster_path) { group_cluster_path(clusterable, cluster) } + end + end +end diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 00ac7c72a11..5fa23dbb998 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -80,8 +80,8 @@ describe 'User accepts a merge request', :js do end it 'accepts a merge request' do - click_button('Modify commit message') - fill_in('Commit message', with: 'wow such merge') + find('.js-mr-widget-commits-count').click + fill_in('merge-message-edit', with: 'wow such merge') click_button('Merge') diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb index 8d2d4279d3c..c6b11fce388 100644 --- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb +++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb @@ -13,7 +13,7 @@ describe 'Merge request < User customizes merge commit message', :js do description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" ) end - let(:textbox) { page.find(:css, '.js-commit-message', visible: false) } + let(:textbox) { page.find(:css, '#merge-message-edit', visible: false) } let(:default_message) do [ "Merge branch 'feature' into 'master'", @@ -38,16 +38,16 @@ describe 'Merge request < User customizes merge commit message', :js do end it 'toggles commit message between message with description and without description' do - expect(page).not_to have_selector('.js-commit-message') - click_button "Modify commit message" + expect(page).not_to have_selector('#merge-message-edit') + first('.js-mr-widget-commits-count').click expect(textbox).to be_visible expect(textbox.value).to eq(default_message) - click_link "Include description in commit message" + check('Include merge request description') expect(textbox.value).to eq(message_with_description) - click_link "Don't include description in commit message" + uncheck('Include merge request description') expect(textbox.value).to eq(default_message) end diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 50c723776a3..16c058ab6bd 100644 --- a/spec/features/merge_request/user_resolves_conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -37,6 +37,8 @@ describe 'Merge request > User resolves conflicts', :js do click_on 'Changes' wait_for_requests + find('.js-toggle-tree-list').click + within find('.diff-file', text: 'files/ruby/popen.rb') do expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }") expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }") diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb index 63d8decc2d2..aa91ade46ca 100644 --- a/spec/features/merge_request/user_sees_versions_spec.rb +++ b/spec/features/merge_request/user_sees_versions_spec.rb @@ -42,7 +42,7 @@ describe 'Merge request > User sees versions', :js do expect(page).to have_content 'latest version' end - expect(page).to have_content '8 changed files' + expect(page).to have_content '8 Files' end it_behaves_like 'allows commenting', @@ -76,7 +76,7 @@ describe 'Merge request > User sees versions', :js do end it 'shows comments that were last relevant at that version' do - expect(page).to have_content '5 changed files' + expect(page).to have_content '5 Files' position = Gitlab::Diff::Position.new( old_path: ".gitmodules", @@ -120,8 +120,15 @@ describe 'Merge request > User sees versions', :js do diff_id: merge_request_diff3.id, start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' ) - expect(page).to have_content '4 changed files' - expect(page).to have_content '15 additions 6 deletions' + expect(page).to have_content '4 Files' + + additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') + .ancestor('.diff-stats-group').text + deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion') + .ancestor('.diff-stats-group').text + + expect(additions_content).to eq '15' + expect(deletions_content).to eq '6' position = Gitlab::Diff::Position.new( old_path: ".gitmodules", @@ -141,8 +148,14 @@ describe 'Merge request > User sees versions', :js do end it 'show diff between new and old version' do - expect(page).to have_content '4 changed files' - expect(page).to have_content '15 additions 6 deletions' + additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') + .ancestor('.diff-stats-group').text + deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion') + .ancestor('.diff-stats-group').text + + expect(page).to have_content '4 Files' + expect(additions_content).to eq '15' + expect(deletions_content).to eq '6' end it 'returns to latest version when "Show latest version" button is clicked' do @@ -150,7 +163,7 @@ describe 'Merge request > User sees versions', :js do page.within '.mr-version-dropdown' do expect(page).to have_content 'latest version' end - expect(page).to have_content '8 changed files' + expect(page).to have_content '8 Files' end it_behaves_like 'allows commenting', @@ -176,7 +189,7 @@ describe 'Merge request > User sees versions', :js do find('.btn-default').click click_link 'version 1' end - expect(page).to have_content '0 changed files' + expect(page).to have_content '0 Files' end end @@ -202,7 +215,7 @@ describe 'Merge request > User sees versions', :js do expect(page).to have_content 'version 1' end - expect(page).to have_content '0 changed files' + expect(page).to have_content '0 Files' end end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 6f8ec0015ad..4c85abe9971 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -98,14 +98,12 @@ describe "Projects > Settings > Pipelines settings" do expect(page).not_to have_content('instance enabled') expect(find_field('project_auto_devops_attributes_enabled')).not_to be_checked check 'Default to Auto DevOps pipeline' - fill_in('project_auto_devops_attributes_domain', with: 'test.com') click_on 'Save changes' end expect(page.status_code).to eq(200) expect(project.auto_devops).to be_present expect(project.auto_devops).to be_enabled - expect(project.auto_devops.domain).to eq('test.com') page.within '#autodevops-settings' do expect(find_field('project_auto_devops_attributes_enabled')).to be_checked @@ -113,29 +111,6 @@ describe "Projects > Settings > Pipelines settings" do end end end - - context 'when there is a cluster with ingress and external_ip' do - before do - cluster = create(:cluster, projects: [project]) - cluster.create_application_ingress!(external_ip: '192.168.1.100') - end - - it 'shows the help text with the nip.io domain as an alternative to custom domain' do - visit project_settings_ci_cd_path(project) - expect(page).to have_content('192.168.1.100.nip.io can be used as an alternative to a custom domain') - end - end - - context 'when there is no ingress' do - before do - create(:cluster, projects: [project]) - end - - it 'alternative to custom domain is not shown' do - visit project_settings_ci_cd_path(project) - expect(page).not_to have_content('can be used as an alternative to a custom domain') - end - end end describe 'runners registration token' do diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb index eb974c7c7fd..74fdfcf492e 100644 --- a/spec/features/snippets/show_spec.rb +++ b/spec/features/snippets/show_spec.rb @@ -79,14 +79,6 @@ describe 'Snippet', :js do expect(page).not_to have_xpath("//ol//li//ul") end end - - context 'with cached CommonMark html' do - let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } - - it 'renders correctly' do - expect(page).not_to have_xpath("//ol//li//ul") - end - end end context 'switching to the simple viewer' do diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 138a6c5ed6b..5ebc09a96dc 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -34,7 +34,8 @@ "status_reason": { "type": ["string", "null"] }, "external_ip": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] }, - "email": { "type": ["string", "null"] } + "email": { "type": ["string", "null"] }, + "update_available": { "type": ["boolean", "null"] } }, "required" : [ "name", "status" ] } diff --git a/spec/fixtures/api/schemas/public_api/v4/group_labels.json b/spec/fixtures/api/schemas/public_api/v4/group_labels.json new file mode 100644 index 00000000000..f6c327abfdd --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/group_labels.json @@ -0,0 +1,18 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties" : { + "id" : { "type": "integer" }, + "name" : { "type": "string "}, + "color" : { "type": "string "}, + "description" : { "type": "string "}, + "open_issues_count" : { "type": "integer "}, + "closed_issues_count" : { "type": "integer "}, + "open_merge_requests_count" : { "type": "integer "}, + "subscribed" : { "type": "boolean" }, + "priority" : { "type": "null" } + }, + "additionalProperties": false + } +} diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 75c30dbfe48..223e562238d 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -90,39 +90,4 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end end - - describe '.auto_devops_warning_message' do - subject { helper.auto_devops_warning_message(project) } - - context 'when the service is missing' do - before do - allow(helper).to receive(:missing_auto_devops_service?).and_return(true) - end - - context 'when the domain is missing' do - before do - allow(helper).to receive(:missing_auto_devops_domain?).and_return(true) - end - - it { is_expected.to match(/Auto Review Apps and Auto Deploy need a domain name and a .* to work correctly./) } - end - - context 'when the domain is not missing' do - before do - allow(helper).to receive(:missing_auto_devops_domain?).and_return(false) - end - - it { is_expected.to match(/Auto Review Apps and Auto Deploy need a .* to work correctly./) } - end - end - - context 'when the domain is missing' do - before do - allow(helper).to receive(:missing_auto_devops_service?).and_return(false) - allow(helper).to receive(:missing_auto_devops_domain?).and_return(true) - end - - it { is_expected.to eq('Auto Review Apps and Auto Deploy need a domain name to work correctly.') } - end - end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index c112c8ed633..4c395248644 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -35,6 +35,30 @@ describe PreferencesHelper do end end + describe '#first_day_of_week_choices' do + it 'returns Sunday and Monday as choices' do + expect(helper.first_day_of_week_choices).to eq [ + ['Sunday', 0], + ['Monday', 1] + ] + end + end + + describe '#first_day_of_week_choices_with_default' do + it 'returns choices including system default' do + expect(helper.first_day_of_week_choices_with_default).to eq [ + ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1] + ] + end + + it 'returns choices including system default set to Monday' do + stub_application_setting(first_day_of_week: 1) + expect(helper.first_day_of_week_choices_with_default).to eq [ + ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1] + ] + end + end + describe '#user_application_theme' do context 'with a user' do it "returns user's theme's css_class" do diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js index d1f4a1cebb4..8cb9713964e 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/javascripts/clusters/components/application_row_spec.js @@ -208,6 +208,144 @@ describe('Application Row', () => { }); }); + describe('Upgrade button', () => { + it('has indeterminate state on page load', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: null, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + expect(upgradeBtn).toBe(null); + }); + + it('has enabled "Upgrade" when "upgradeAvailable" is true', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + upgradeAvailable: true, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + expect(upgradeBtn).not.toBe(null); + expect(upgradeBtn.innerHTML).toContain('Upgrade'); + }); + + it('has enabled "Retry upgrade" when APPLICATION_STATUS.UPDATE_ERRORED', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATE_ERRORED, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + expect(upgradeBtn).not.toBe(null); + expect(vm.upgradeFailed).toBe(true); + expect(upgradeBtn.innerHTML).toContain('Retry upgrade'); + }); + + it('has disabled "Retry upgrade" when APPLICATION_STATUS.UPDATING', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATING, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + expect(upgradeBtn).not.toBe(null); + expect(vm.isUpgrading).toBe(true); + expect(upgradeBtn.innerHTML).toContain('Upgrading'); + }); + + it('clicking upgrade button emits event', () => { + spyOn(eventHub, '$emit'); + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATE_ERRORED, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + upgradeBtn.click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('upgradeApplication', { + id: DEFAULT_APPLICATION_STATE.id, + params: {}, + }); + }); + + it('clicking disabled upgrade button emits nothing', () => { + spyOn(eventHub, '$emit'); + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATING, + }); + const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button'); + + upgradeBtn.click(); + + expect(eventHub.$emit).not.toHaveBeenCalled(); + }); + + it('displays an error message if application upgrade failed', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + title: 'GitLab Runner', + status: APPLICATION_STATUS.UPDATE_ERRORED, + }); + const failureMessage = vm.$el.querySelector( + '.js-cluster-application-upgrade-failure-message', + ); + + expect(failureMessage).not.toBe(null); + expect(failureMessage.innerHTML).toContain( + 'Something went wrong when upgrading GitLab Runner. Please check the logs and try again.', + ); + }); + }); + + describe('Version', () => { + it('displays a version number if application has been upgraded', () => { + const version = '0.1.45'; + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATED, + version, + }); + const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); + const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); + + expect(upgradeDetails.innerHTML).toContain('Upgraded'); + expect(versionEl).not.toBe(null); + expect(versionEl.innerHTML).toContain(version); + }); + + it('contains a link to the chart repo if application has been upgraded', () => { + const version = '0.1.45'; + const chartRepo = 'https://gitlab.com/charts/gitlab-runner'; + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATED, + chartRepo, + version, + }); + const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); + + expect(versionEl.href).toEqual(chartRepo); + expect(versionEl.target).toEqual('_blank'); + }); + + it('does not display a version number if application upgrade failed', () => { + const version = '0.1.45'; + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.UPDATE_ERRORED, + version, + }); + const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details'); + const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version'); + + expect(upgradeDetails.innerHTML).toContain('failed'); + expect(versionEl).toBe(null); + }); + }); + describe('Error block', () => { it('does not show error block when there is no error', () => { vm = mountComponent(ApplicationRow, { diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index dfce2656e4c..37a4d6614f6 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -85,6 +85,9 @@ describe('Clusters Store', () => { statusReason: mockResponseData.applications[2].status_reason, requestStatus: null, requestReason: null, + version: mockResponseData.applications[2].version, + upgradeAvailable: mockResponseData.applications[2].update_available, + chartRepo: 'https://gitlab.com/charts/gitlab-runner', }, prometheus: { title: 'Prometheus', diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 2f0385454d7..e886f962d2f 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -10,6 +10,10 @@ describe('CompareVersions', () => { const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; beforeEach(() => { + store.state.diffs.addedLines = 10; + store.state.diffs.removedLines = 20; + store.state.diffs.diffFiles.push('test'); + vm = createComponentWithStore(Vue.extend(CompareVersionsComponent), store, { mergeRequestDiffs: diffsMockData, mergeRequestDiff: diffsMockData[0], diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index b77907ff26f..787a81fd88f 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -24,6 +24,10 @@ describe('diff_file_header', () => { beforeEach(() => { const diffFile = diffDiscussionMock.diff_file; + + diffFile.added_lines = 2; + diffFile.removed_lines = 1; + props = { diffFile: { ...diffFile }, canCurrentUserFork: false, diff --git a/spec/javascripts/diffs/components/diff_stats_spec.js b/spec/javascripts/diffs/components/diff_stats_spec.js new file mode 100644 index 00000000000..984b3026209 --- /dev/null +++ b/spec/javascripts/diffs/components/diff_stats_spec.js @@ -0,0 +1,33 @@ +import { shallowMount } from '@vue/test-utils'; +import DiffStats from '~/diffs/components/diff_stats.vue'; + +describe('diff_stats', () => { + it('does not render a group if diffFileLengths is not passed in', () => { + const wrapper = shallowMount(DiffStats, { + propsData: { + addedLines: 1, + removedLines: 2, + }, + }); + const groups = wrapper.findAll('.diff-stats-group'); + + expect(groups.length).toBe(2); + }); + + it('shows amount of files changed, lines added and lines removed when passed all props', () => { + const wrapper = shallowMount(DiffStats, { + propsData: { + addedLines: 100, + removedLines: 200, + diffFilesLength: 300, + }, + }); + const additions = wrapper.find('icon-stub[name="file-addition"]').element.parentNode; + const deletions = wrapper.find('icon-stub[name="file-deletion"]').element.parentNode; + const filesChanged = wrapper.find('icon-stub[name="doc-code"]').element.parentNode; + + expect(additions.textContent).toContain('100'); + expect(deletions.textContent).toContain('200'); + expect(filesChanged.textContent).toContain('300'); + }); +}); diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index c5ef48a81e9..9e556698f34 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -35,12 +35,6 @@ describe('Diffs tree list component', () => { vm.$destroy(); }); - it('renders diff stats', () => { - expect(vm.$el.textContent).toContain('1 changed file'); - expect(vm.$el.textContent).toContain('10 additions'); - expect(vm.$el.textContent).toContain('20 deletions'); - }); - it('renders empty text', () => { expect(vm.$el.textContent).toContain('No files found'); }); diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 7618c2f50ce..a89e50045da 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -25,7 +25,6 @@ describe('Environment item', () => { component = new EnvironmentItem({ propsData: { model: mockItem, - canCreateDeployment: false, canReadEnvironment: true, service: {}, }, @@ -117,7 +116,6 @@ describe('Environment item', () => { component = new EnvironmentItem({ propsData: { model: environment, - canCreateDeployment: true, canReadEnvironment: true, service: {}, }, diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js index 0e5e50a59a5..52895f35f3a 100644 --- a/spec/javascripts/environments/environment_table_spec.js +++ b/spec/javascripts/environments/environment_table_spec.js @@ -26,7 +26,6 @@ describe('Environment table', () => { vm = mountComponent(Component, { environments: [mockItem], - canCreateDeployment: false, canReadEnvironment: true, }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index e2d81eb454a..9220f7a264f 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -9,7 +9,6 @@ describe('Environment', () => { const mockData = { endpoint: 'environments.json', canCreateEnvironment: true, - canCreateDeployment: true, canReadEnvironment: true, cssContainerClass: 'container', newEnvironmentPath: 'environments/new', diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 7f0a9475d5f..d9ee7e74e28 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -13,7 +13,6 @@ describe('Environments Folder View', () => { const mockData = { endpoint: 'environments.json', folderName: 'review', - canCreateDeployment: true, canReadEnvironment: true, cssContainerClass: 'container', }; diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js new file mode 100644 index 00000000000..19e27388eeb --- /dev/null +++ b/spec/javascripts/helpers/vue_test_utils_helper.js @@ -0,0 +1,19 @@ +/* eslint-disable import/prefer-default-export */ + +const vNodeContainsText = (vnode, text) => + (vnode.text && vnode.text.includes(text)) || + (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length); + +/** + * Determines whether a `shallowMount` Wrapper contains text + * within one of it's slots. This will also work on Wrappers + * acquired with `find()`, but only if it's parent Wrapper + * was shallowMounted. + * NOTE: Prefer checking the rendered output of a component + * wherever possible using something like `text()` instead. + * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted) + * @param {String} slotName + * @param {String} text + */ +export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) => + !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length; diff --git a/spec/javascripts/helpers/vue_test_utils_helper_spec.js b/spec/javascripts/helpers/vue_test_utils_helper_spec.js new file mode 100644 index 00000000000..41714066da5 --- /dev/null +++ b/spec/javascripts/helpers/vue_test_utils_helper_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { shallowWrapperContainsSlotText } from './vue_test_utils_helper'; + +describe('Vue test utils helpers', () => { + describe('shallowWrapperContainsSlotText', () => { + const mockText = 'text'; + const mockSlot = `<div>${mockText}</div>`; + let mockComponent; + + beforeEach(() => { + mockComponent = shallowMount( + { + render(h) { + h(`<div>mockedComponent</div>`); + }, + }, + { + slots: { + default: mockText, + namedSlot: mockSlot, + }, + }, + ); + }); + + it('finds text within shallowWrapper default slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'default', mockText)).toBe(true); + }); + + it('finds text within shallowWrapper named slot', () => { + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', mockText)).toBe(true); + }); + + it('returns false when text is not present', () => { + const searchText = 'absent'; + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + + it('searches with case-sensitivity', () => { + const searchText = mockText.toUpperCase(); + + expect(shallowWrapperContainsSlotText(mockComponent, 'default', searchText)).toBe(false); + expect(shallowWrapperContainsSlotText(mockComponent, 'namedSlot', searchText)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 72716b97f5f..2eeed6770be 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -21,7 +21,8 @@ describe('Description component', () => { if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); metaData.classList.add('issuable-meta'); - metaData.innerHTML = '<span id="task_status"></span><span id="task_status_short"></span>'; + metaData.innerHTML = + '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>'; document.body.appendChild(metaData); } @@ -33,6 +34,10 @@ describe('Description component', () => { vm.$destroy(); }); + afterAll(() => { + $('.issuable-meta .flash-container').remove(); + }); + it('animates description changes', done => { vm.descriptionHtml = 'changed'; @@ -192,12 +197,11 @@ describe('Description component', () => { it('should create flash notification and emit an event to parent', () => { const msg = 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.'; - spyOn(window, 'Flash'); spyOn(vm, '$emit'); vm.taskListUpdateError(); - expect(window.Flash).toHaveBeenCalledWith(msg); + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg); expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed'); }); }); diff --git a/spec/javascripts/lib/utils/file_upload_spec.js b/spec/javascripts/lib/utils/file_upload_spec.js index 92c9cc70aaf..8f7092f63de 100644 --- a/spec/javascripts/lib/utils/file_upload_spec.js +++ b/spec/javascripts/lib/utils/file_upload_spec.js @@ -9,28 +9,56 @@ describe('File upload', () => { <span class="js-filename"></span> </form> `); + }); + + describe('when there is a matching button and input', () => { + beforeEach(() => { + fileUpload('.js-button', '.js-input'); + }); + + it('clicks file input after clicking button', () => { + const btn = document.querySelector('.js-button'); + const input = document.querySelector('.js-input'); + + spyOn(input, 'click'); + + btn.click(); + + expect(input.click).toHaveBeenCalled(); + }); + + it('updates file name text', () => { + const input = document.querySelector('.js-input'); - fileUpload('.js-button', '.js-input'); + input.value = 'path/to/file/index.js'; + + input.dispatchEvent(new CustomEvent('change')); + + expect(document.querySelector('.js-filename').textContent).toEqual('index.js'); + }); }); - it('clicks file input after clicking button', () => { - const btn = document.querySelector('.js-button'); + it('fails gracefully when there is no matching button', () => { const input = document.querySelector('.js-input'); + const btn = document.querySelector('.js-button'); + fileUpload('.js-not-button', '.js-input'); spyOn(input, 'click'); btn.click(); - expect(input.click).toHaveBeenCalled(); + expect(input.click).not.toHaveBeenCalled(); }); - it('updates file name text', () => { + it('fails gracefully when there is no matching input', () => { const input = document.querySelector('.js-input'); + const btn = document.querySelector('.js-button'); + fileUpload('.js-button', '.js-not-input'); - input.value = 'path/to/file/index.js'; + spyOn(input, 'click'); - input.dispatchEvent(new CustomEvent('change')); + btn.click(); - expect(document.querySelector('.js-filename').textContent).toEqual('index.js'); + expect(input.click).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 32623d1781a..ab809930804 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -40,30 +40,51 @@ describe('MergeRequest', function() { expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - it('submits an ajax request on tasklist:changed', done => { + describe('tasklist', () => { const lineNumber = 8; const lineSource = '- [ ] item 8'; const index = 3; const checked = true; - $('.js-task-list-field').trigger({ - type: 'tasklist:changed', - detail: { lineNumber, lineSource, index, checked }, + it('submits an ajax request on tasklist:changed', done => { + $('.js-task-list-field').trigger({ + type: 'tasklist:changed', + detail: { lineNumber, lineSource, index, checked }, + }); + + setTimeout(() => { + expect(axios.patch).toHaveBeenCalledWith( + `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, + { + merge_request: { + description: '- [ ] Task List Item', + lock_version: undefined, + update_task: { line_number: lineNumber, line_source: lineSource, index, checked }, + }, + }, + ); + + done(); + }); }); - setTimeout(() => { - expect(axios.patch).toHaveBeenCalledWith( - `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, - { - merge_request: { - description: '- [ ] Task List Item', - lock_version: undefined, - update_task: { line_number: lineNumber, line_source: lineSource, index, checked }, - }, - }, - ); + it('shows an error notification when tasklist update failed', done => { + mock + .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`) + .reply(409, {}); + + $('.js-task-list-field').trigger({ + type: 'tasklist:changed', + detail: { lineNumber, lineSource, index, checked }, + }); + + setTimeout(() => { + expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe( + 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.', + ); - done(); + done(); + }); }); }); }); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js new file mode 100644 index 00000000000..0b36fc9f5f7 --- /dev/null +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -0,0 +1,220 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAreaChart } from '@gitlab/ui/dist/charts'; +import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; +import Area from '~/monitoring/components/charts/area.vue'; +import MonitoringStore from '~/monitoring/stores/monitoring_store'; +import MonitoringMock, { deploymentData } from '../mock_data'; + +describe('Area component', () => { + const mockWidgets = 'mockWidgets'; + let mockGraphData; + let areaChart; + let spriteSpy; + + beforeEach(() => { + const store = new MonitoringStore(); + store.storeMetrics(MonitoringMock.data); + store.storeDeploymentData(deploymentData); + + [mockGraphData] = store.groups[0].metrics; + + areaChart = shallowMount(Area, { + propsData: { + graphData: mockGraphData, + containerWidth: 0, + deploymentData: store.deploymentData, + }, + slots: { + default: mockWidgets, + }, + }); + + spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( + () => new Promise(resolve => resolve()), + ); + }); + + afterEach(() => { + areaChart.destroy(); + }); + + it('renders chart title', () => { + expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets); + }); + + describe('wrapped components', () => { + describe('GitLab UI area chart', () => { + let glAreaChart; + + beforeEach(() => { + glAreaChart = areaChart.find(GlAreaChart); + }); + + it('is a Vue instance', () => { + expect(glAreaChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glAreaChart.props(); + + expect(props.data).toBe(areaChart.vm.chartData); + expect(props.option).toBe(areaChart.vm.chartOptions); + expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText); + expect(props.thresholds).toBe(areaChart.props('alertData')); + }); + + it('recieves a tooltip title', () => { + const mockTitle = 'mockTitle'; + areaChart.vm.tooltip.title = mockTitle; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', mockTitle)).toBe(true); + }); + + it('recieves tooltip content', () => { + const mockContent = 'mockContent'; + areaChart.vm.tooltip.content = mockContent; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockContent)).toBe( + true, + ); + }); + + describe('when tooltip is showing deployment data', () => { + beforeEach(() => { + areaChart.vm.tooltip.isDeployment = true; + }); + + it('uses deployment title', () => { + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', 'Deployed')).toBe( + true, + ); + }); + + it('renders commit sha in tooltip content', () => { + const mockSha = 'mockSha'; + areaChart.vm.tooltip.sha = mockSha; + + expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockSha)).toBe(true); + }); + }); + }); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = deploymentData[0].created_at; + const generateSeriesData = type => ({ + seriesData: [ + { + componentSubType: type, + value: [mockDate, 5.55555], + }, + ], + value: mockDate, + }); + + describe('series is of line type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('line')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip content', () => { + expect(areaChart.vm.tooltip.content).toBe('CPU (Cores) 5.556'); + }); + }); + + describe('series is of scatter type', () => { + beforeEach(() => { + areaChart.vm.formatTooltipText(generateSeriesData('scatter')); + }); + + it('formats tooltip title', () => { + expect(areaChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip sha', () => { + expect(areaChart.vm.tooltip.sha).toBe('f5bcd1d9'); + }); + }); + }); + + describe('getScatterSymbol', () => { + beforeEach(() => { + areaChart.vm.getScatterSymbol(); + }); + + it('gets rocket svg path content for use as deployment data symbol', () => { + expect(spriteSpy).toHaveBeenCalledWith('rocket'); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + const mockHeight = 144; + + beforeEach(() => { + spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({ + width: mockWidth, + height: mockHeight, + })); + areaChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(areaChart.vm.width).toBe(mockWidth); + }); + + it('sets area chart height', () => { + expect(areaChart.vm.height).toBe(mockHeight); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + it('utilizes all data points', () => { + expect(Object.keys(areaChart.vm.chartData)).toEqual(['Cores']); + expect(areaChart.vm.chartData.Cores.length).toBe(297); + }); + + it('creates valid data', () => { + const data = areaChart.vm.chartData.Cores; + + expect( + data.filter(([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number') + .length, + ).toBe(data.length); + }); + }); + + describe('scatterSeries', () => { + it('utilizes deployment data', () => { + expect(areaChart.vm.scatterSeries.data).toEqual([ + ['2017-05-31T21:23:37.881Z', 0], + ['2017-05-30T20:08:04.629Z', 0], + ['2017-05-30T17:42:38.409Z', 0], + ]); + }); + }); + + describe('xAxisLabel', () => { + it('constructs a label for the chart x-axis', () => { + expect(areaChart.vm.xAxisLabel).toBe('Core Usage'); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(areaChart.vm.yAxisLabel).toBe('CPU (Cores)'); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 97b9671c809..b1778029a77 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -25,15 +25,22 @@ export default propsData; describe('Dashboard', () => { let DashboardComponent; + let mock; beforeEach(() => { setFixtures(` <div class="prometheus-graphs"></div> <div class="layout-page"></div> `); + + mock = new MockAdapter(axios); DashboardComponent = Vue.extend(Dashboard); }); + afterEach(() => { + mock.restore(); + }); + describe('no metrics are available yet', () => { it('shows a getting started empty state when no metrics are present', () => { const component = new DashboardComponent({ @@ -47,16 +54,10 @@ describe('Dashboard', () => { }); describe('requests information to the server', () => { - let mock; beforeEach(() => { - mock = new MockAdapter(axios); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); }); - afterEach(() => { - mock.restore(); - }); - it('shows up a loading state', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), @@ -152,15 +153,12 @@ describe('Dashboard', () => { }); describe('when the window resizes', () => { - let mock; beforeEach(() => { - mock = new MockAdapter(axios); mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); jasmine.clock().install(); }); afterEach(() => { - mock.restore(); jasmine.clock().uninstall(); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index b4e2cd75d47..ffc7148fde2 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -326,6 +326,7 @@ export const metricsGroupsAPIResponse = { { id: 6, title: 'CPU usage', + y_label: 'CPU', weight: 1, queries: [ { diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 7ae45c40c28..348743081eb 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -165,7 +165,6 @@ export const note = { report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', path: '/gitlab-org/gitlab-ce/notes/546', - cached_markdown_version: 11, }; export const discussionMock = { diff --git a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js b/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js new file mode 100644 index 00000000000..994d6255324 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js @@ -0,0 +1,85 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; + +const localVue = createLocalVue(); +const testCommitMessage = 'Test commit message'; +const testLabel = 'Test label'; +const testInputId = 'test-input-id'; + +describe('Commits edit component', () => { + let wrapper; + + const createComponent = (slots = {}) => { + wrapper = shallowMount(localVue.extend(CommitEdit), { + localVue, + sync: false, + propsData: { + value: testCommitMessage, + label: testLabel, + inputId: testInputId, + }, + slots: { + ...slots, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findTextarea = () => wrapper.find('.form-control'); + + it('has a correct label', () => { + const labelElement = wrapper.find('.col-form-label'); + + expect(labelElement.text()).toBe(testLabel); + }); + + describe('textarea', () => { + it('has a correct ID', () => { + expect(findTextarea().attributes('id')).toBe(testInputId); + }); + + it('has a correct value', () => { + expect(findTextarea().element.value).toBe(testCommitMessage); + }); + + it('emits an input event and receives changed value', () => { + const changedCommitMessage = 'Changed commit message'; + + findTextarea().element.value = changedCommitMessage; + findTextarea().trigger('input'); + + expect(wrapper.emitted().input[0]).toEqual([changedCommitMessage]); + expect(findTextarea().element.value).toBe(changedCommitMessage); + }); + }); + + describe('when slots are present', () => { + beforeEach(() => { + createComponent({ + header: `<div class="test-header">${testCommitMessage}</div>`, + checkbox: `<label slot="checkbox" class="test-checkbox">${testLabel}</label >`, + }); + }); + + it('renders header slot correctly', () => { + const headerSlotElement = wrapper.find('.test-header'); + + expect(headerSlotElement.exists()).toBe(true); + expect(headerSlotElement.text()).toBe(testCommitMessage); + }); + + it('renders checkbox slot correctly', () => { + const checkboxSlotElement = wrapper.find('.test-checkbox'); + + expect(checkboxSlotElement.exists()).toBe(true); + expect(checkboxSlotElement.text()).toBe(testLabel); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js new file mode 100644 index 00000000000..daf1cc2d98b --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js @@ -0,0 +1,61 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlDropdownItem } from '@gitlab/ui'; +import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; + +const localVue = createLocalVue(); +const commits = [ + { + title: 'Commit 1', + short_id: '78d5b7', + message: 'Update test.txt', + }, + { + title: 'Commit 2', + short_id: '34cbe28b', + message: 'Fixed test', + }, + { + title: 'Commit 3', + short_id: 'fa42932a', + message: 'Added changelog', + }, +]; + +describe('Commits message dropdown component', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(localVue.extend(CommitMessageDropdown), { + localVue, + sync: false, + propsData: { + commits, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findDropdownElements = () => wrapper.findAll(GlDropdownItem); + const findFirstDropdownElement = () => findDropdownElements().at(0); + + it('should have 3 elements in dropdown list', () => { + expect(findDropdownElements().length).toBe(3); + }); + + it('should have correct message for the first dropdown list element', () => { + expect(findFirstDropdownElement().text()).toBe('78d5b7 Commit 1'); + }); + + it('should emit a commit title on selecting commit', () => { + findFirstDropdownElement().vm.$emit('click'); + + expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js new file mode 100644 index 00000000000..5cf6408cf34 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js @@ -0,0 +1,110 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const localVue = createLocalVue(); + +describe('Commits header component', () => { + let wrapper; + + const createComponent = props => { + wrapper = shallowMount(localVue.extend(CommitsHeader), { + localVue, + sync: false, + propsData: { + isSquashEnabled: false, + targetBranch: 'master', + commitsCount: 5, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count'); + const findCommitToggle = () => wrapper.find('.commit-edit-toggle'); + const findIcon = () => wrapper.find(Icon); + const findCommitsCountMessage = () => wrapper.find('.commits-count-message'); + const findTargetBranchMessage = () => wrapper.find('.label-branch'); + const findModifyButton = () => wrapper.find('.modify-message-button'); + + describe('when collapsed', () => { + it('toggle has aria-label equal to Expand', () => { + createComponent(); + + expect(findCommitToggle().attributes('aria-label')).toBe('Expand'); + }); + + it('has a chevron-right icon', () => { + createComponent(); + wrapper.setData({ expanded: false }); + + expect(findIcon().props('name')).toBe('chevron-right'); + }); + + describe('when squash is disabled', () => { + beforeEach(() => { + createComponent(); + }); + + it('has commits count message showing correct amount of commits', () => { + expect(findCommitsCountMessage().text()).toBe('5 commits'); + }); + + it('has button with modify merge commit message', () => { + expect(findModifyButton().text()).toBe('Modify merge commit'); + }); + }); + + describe('when squash is enabled', () => { + beforeEach(() => { + createComponent({ isSquashEnabled: true }); + }); + + it('has commits count message showing one commit when squash is enabled', () => { + expect(findCommitsCountMessage().text()).toBe('1 commit'); + }); + + it('has button with modify commit messages text', () => { + expect(findModifyButton().text()).toBe('Modify commit messages'); + }); + }); + + it('has correct target branch displayed', () => { + createComponent(); + + expect(findTargetBranchMessage().text()).toBe('master'); + }); + }); + + describe('when expanded', () => { + beforeEach(() => { + createComponent(); + wrapper.setData({ expanded: true }); + }); + + it('toggle has aria-label equal to collapse', done => { + wrapper.vm.$nextTick(() => { + expect(findCommitToggle().attributes('aria-label')).toBe('Collapse'); + done(); + }); + }); + + it('has a chevron-down icon', done => { + wrapper.vm.$nextTick(() => { + expect(findIcon().props('name')).toBe('chevron-down'); + done(); + }); + }); + + it('has a collapse text', done => { + wrapper.vm.$nextTick(() => { + expect(findHeaderWrapper().text()).toBe('Collapse'); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index e387367d1a2..631da202d1d 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,10 +1,14 @@ import Vue from 'vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; +import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; +import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; +import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import { createLocalVue, shallowMount } from '@vue/test-utils'; const commitMessage = 'This is the commit message'; +const squashCommitMessage = 'This is the squash commit message'; const commitMessageWithDescription = 'This is the commit message description'; const createTestMr = customConfig => { const mr = { @@ -19,9 +23,11 @@ const createTestMr = customConfig => { sha: '12345678', squash: false, commitMessage, + squashCommitMessage, commitMessageWithDescription, shouldRemoveSourceBranch: true, canRemoveSourceBranch: false, + targetBranch: 'master', }; Object.assign(mr, customConfig.mr); @@ -98,21 +104,6 @@ describe('ReadyToMerge', () => { }); }); - describe('commitMessageLinkTitle', () => { - const withDesc = 'Include description in commit message'; - const withoutDesc = "Don't include description in commit message"; - - it('should return message with description', () => { - expect(vm.commitMessageLinkTitle).toEqual(withDesc); - }); - - it('should return message without description', () => { - vm.useCommitMessageWithDescription = true; - - expect(vm.commitMessageLinkTitle).toEqual(withoutDesc); - }); - }); - describe('status', () => { it('defaults to success', () => { vm.mr.pipeline = true; @@ -279,55 +270,43 @@ describe('ReadyToMerge', () => { vm.mr.isMergeAllowed = false; vm.mr.isPipelineActive = false; - expect(vm.shouldShowMergeControls()).toBeFalsy(); + expect(vm.shouldShowMergeControls).toBeFalsy(); }); it('should return true when the build succeeded or build not required to succeed', () => { vm.mr.isMergeAllowed = true; vm.mr.isPipelineActive = false; - expect(vm.shouldShowMergeControls()).toBeTruthy(); + expect(vm.shouldShowMergeControls).toBeTruthy(); }); it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { vm.mr.isMergeAllowed = false; vm.mr.isPipelineActive = true; - expect(vm.shouldShowMergeControls()).toBeTruthy(); + expect(vm.shouldShowMergeControls).toBeTruthy(); }); it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { vm.mr.isMergeAllowed = true; vm.mr.isPipelineActive = true; - expect(vm.shouldShowMergeControls()).toBeTruthy(); + expect(vm.shouldShowMergeControls).toBeTruthy(); }); }); - describe('updateCommitMessage', () => { + describe('updateMergeCommitMessage', () => { it('should revert flag and change commitMessage', () => { - expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.commitMessage).toEqual(commitMessage); - vm.updateCommitMessage(); + vm.updateMergeCommitMessage(true); - expect(vm.useCommitMessageWithDescription).toBeTruthy(); expect(vm.commitMessage).toEqual(commitMessageWithDescription); - vm.updateCommitMessage(); + vm.updateMergeCommitMessage(false); - expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.commitMessage).toEqual(commitMessage); }); }); - describe('toggleCommitMessageEditor', () => { - it('should toggle showCommitMessageEditor flag', () => { - expect(vm.showCommitMessageEditor).toBeFalsy(); - vm.toggleCommitMessageEditor(); - - expect(vm.showCommitMessageEditor).toBeTruthy(); - }); - }); - describe('handleMergeButtonClick', () => { const returnPromise = status => new Promise(resolve => { @@ -623,7 +602,7 @@ describe('ReadyToMerge', () => { }); }); - describe('Squash checkbox component', () => { + describe('render children components', () => { let wrapper; const localVue = createLocalVue(); @@ -642,25 +621,101 @@ describe('ReadyToMerge', () => { }); const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); + const findCommitsHeaderElement = () => wrapper.find(CommitsHeader); + const findCommitEditElements = () => wrapper.findAll(CommitEdit); + const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown); + + describe('squash checkbox', () => { + it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { + createLocalComponent({ + mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + }); - it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { - createLocalComponent({ - mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + expect(findCheckboxElement().exists()).toBeTruthy(); }); - expect(findCheckboxElement().exists()).toBeTruthy(); + it('should not be rendered when squash before merge is disabled', () => { + createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + + it('should not be rendered when there is only 1 commit', () => { + createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); }); - it('should not be rendered when squash before merge is disabled', () => { - createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + describe('commits count collapsible header', () => { + it('should be rendered if fast-forward is disabled', () => { + createLocalComponent(); - expect(findCheckboxElement().exists()).toBeFalsy(); + expect(findCommitsHeaderElement().exists()).toBeTruthy(); + }); + + it('should not be rendered if fast-forward is enabled', () => { + createLocalComponent({ mr: { ffOnlyEnabled: true } }); + + expect(findCommitsHeaderElement().exists()).toBeFalsy(); + }); }); - it('should not be rendered when there is only 1 commit', () => { - createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + describe('commits edit components', () => { + it('should have one edit component when squash is disabled', () => { + createLocalComponent(); + + expect(findCommitEditElements().length).toBe(1); + }); - expect(findCheckboxElement().exists()).toBeFalsy(); + const findFirstCommitEditLabel = () => + findCommitEditElements() + .at(0) + .props('label'); + + it('should have two edit components when squash is enabled', () => { + createLocalComponent({ + mr: { + commitsCount: 2, + squash: true, + enableSquashBeforeMerge: true, + }, + }); + + expect(findCommitEditElements().length).toBe(2); + }); + + it('should have correct edit merge commit label', () => { + createLocalComponent(); + + expect(findFirstCommitEditLabel()).toBe('Merge commit message'); + }); + + it('should have correct edit squash commit label', () => { + createLocalComponent({ + mr: { + commitsCount: 2, + squash: true, + enableSquashBeforeMerge: true, + }, + }); + + expect(findFirstCommitEditLabel()).toBe('Squash commit message'); + }); + }); + + describe('commits dropdown', () => { + it('should not be rendered if squash is disabled', () => { + createLocalComponent(); + + expect(findCommitDropdownElement().exists()).toBeFalsy(); + }); + + it('should be rendered if squash is enabled', () => { + createLocalComponent({ mr: { squash: true } }); + + expect(findCommitDropdownElement().exists()).toBeTruthy(); + }); }); }); @@ -696,10 +751,6 @@ describe('ReadyToMerge', () => { expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); }); - it('does not show modify commit message button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); - }); - it('shows message to resolve all items before being allowed to merge', () => { expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); }); @@ -712,7 +763,7 @@ describe('ReadyToMerge', () => { mr: { ffOnlyEnabled: false }, }); - expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeNull(); + expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeNull(); expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); }); @@ -721,7 +772,7 @@ describe('ReadyToMerge', () => { mr: { ffOnlyEnabled: true }, }); - expect(customVm.$el.querySelector('.js-fast-forward-message')).toBeDefined(); + expect(customVm.$el.querySelector('.mr-fast-forward-message')).toBeDefined(); expect(customVm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 75b197fb2ba..6ef07f81705 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -215,12 +215,14 @@ export default { project_archived: false, default_merge_commit_message_with_description: "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", + default_squash_commit_message: 'Test squash commit message', diverged_commits_count: 0, only_allow_merge_if_pipeline_succeeds: false, commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', merge_commit_path: 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', + squash: true, }; export const mockStore = { diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 209a547c3b3..3b52f6666d0 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do ) end - let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } + let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) } describe '#render' do context 'with cache' do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 236808c0b69..a4a6338961e 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::Auth do it 'optional_scopes contains all non-default scopes' do stub_container_registry_config(enabled: true) - expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid] + expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid profile email] end context 'registry_scopes' do diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index afd8f5da39f..a07c5371134 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -61,7 +61,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do let(:wiki_path) { File.join(root_path, "#{hashed_path}.wiki.git") } before do - gitlab_shell.create_repository(repository_storage, hashed_path) + gitlab_shell.create_repository(repository_storage, hashed_path, 'group/project') Gitlab::GitalyClient::StorageSettings.allow_disk_access do repository = Rugged::Repository.new(repo_path) repository.config['gitlab.fullpath'] = 'to/repo' diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 0def685f177..c432cc223b9 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -218,7 +218,7 @@ describe Gitlab::BitbucketImport::Importer do describe 'wiki import' do it 'is skipped when the wiki exists' do expect(project.wiki).to receive(:repository_exists?) { true } - expect(importer.gitlab_shell).not_to receive(:import_repository) + expect(importer.gitlab_shell).not_to receive(:import_wiki_repository) importer.execute @@ -227,11 +227,7 @@ describe Gitlab::BitbucketImport::Importer do it 'imports to the project disk_path' do expect(project.wiki).to receive(:repository_exists?) { false } - expect(importer.gitlab_shell).to receive(:import_repository).with( - project.repository_storage, - project.wiki.disk_path, - project.import_url + '/wiki' - ) + expect(importer.gitlab_shell).to receive(:import_wiki_repository) importer.execute diff --git a/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb b/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb new file mode 100644 index 00000000000..795fd069ab2 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/wiki_formatter_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::WikiFormatter do + let(:project) do + create(:project, + namespace: create(:namespace, path: 'gitlabhq'), + import_url: 'https://xxx@bitbucket.org/gitlabhq/sample.gitlabhq.git') + end + + subject(:wiki) { described_class.new(project) } + + describe '#disk_path' do + it 'appends .wiki to disk path' do + expect(wiki.disk_path).to eq project.wiki.disk_path + end + end + + describe '#full_path' do + it 'appends .wiki to project path' do + expect(wiki.full_path).to eq project.wiki.full_path + end + end + + describe '#import_url' do + it 'returns URL of the wiki repository' do + expect(wiki.import_url).to eq 'https://xxx@bitbucket.org/gitlabhq/sample.gitlabhq.git/wiki' + end + end +end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index e704d1c673c..0010c0304eb 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::Blame, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:blame) do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 1bcec04d28f..a1b5cea88c0 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Blob, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:rugged) do Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH)) end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 0df282d0ae3..0764e525ede 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Branch, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:rugged) do Rugged::Repository.new(File.join(TestEnv.repos_path, repository.relative_path)) end @@ -64,7 +64,7 @@ describe Gitlab::Git::Branch, :seed_helper do context 'with active, stale and future branches' do let(:repository) do - Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') end let(:user) { create(:user) } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index db68062e433..2611ebed25b 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Gitlab::Git::Commit, :seed_helper do include GitHelpers - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:rugged_repo) do Rugged::Repository.new(File.join(TestEnv.repos_path, TEST_REPO_PATH)) end @@ -146,7 +146,7 @@ describe Gitlab::Git::Commit, :seed_helper do end context 'with broken repo' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') } it 'returns nil' do expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 771c71a16a9..65dfb93d0db 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Compare, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 8a4415506c4..1d22329b670 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Diff, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:gitaly_diff) do Gitlab::GitalyClient::Diff.new( from_path: '.gitmodules', diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb index 53ed7c5a13a..e166628d4ca 100644 --- a/spec/lib/gitlab/git/remote_repository_spec.rb +++ b/spec/lib/gitlab/git/remote_repository_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Gitlab::Git::RemoteRepository, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } subject { described_class.new(repository) } describe '#empty?' do using RSpec::Parameterized::TableSyntax where(:repository, :result) do - Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') | false - Gitlab::Git::Repository.new('default', 'does-not-exist.git', '') | true + Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') | false + Gitlab::Git::Repository.new('default', 'does-not-exist.git', '', 'group/project') | true end with_them do @@ -44,11 +44,11 @@ describe Gitlab::Git::RemoteRepository, :seed_helper do using RSpec::Parameterized::TableSyntax where(:other_repository, :result) do - repository | true - Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '') | true - Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '') | false - Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '') | false - Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '') | false + repository | true + Gitlab::Git::Repository.new(repository.storage, repository.relative_path, '', 'group/project') | true + Gitlab::Git::Repository.new('broken', TEST_REPO_PATH, '', 'group/project') | false + Gitlab::Git::Repository.new(repository.storage, 'wrong/relative-path.git', '', 'group/project') | false + Gitlab::Git::Repository.new('broken', 'wrong/relative-path.git', '', 'group/project') | false end with_them do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a19e3e84f83..cf9e0cccc71 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -19,8 +19,10 @@ describe Gitlab::Git::Repository, :seed_helper do end end - let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') } + let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) } + let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) } let(:repository_rugged) { Rugged::Repository.new(repository_path) } let(:storage_path) { TestEnv.repos_path } @@ -434,13 +436,13 @@ describe Gitlab::Git::Repository, :seed_helper do describe '#fetch_repository_as_mirror' do let(:new_repository) do - Gitlab::Git::Repository.new('default', 'my_project.git', '') + Gitlab::Git::Repository.new('default', 'my_project.git', '', 'group/project') end subject { new_repository.fetch_repository_as_mirror(repository) } before do - Gitlab::Shell.new.create_repository('default', 'my_project') + Gitlab::Shell.new.create_repository('default', 'my_project', 'group/project') end after do @@ -497,6 +499,48 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#search_files_by_content' do + let(:repository) { mutable_repository } + let(:repository_rugged) { mutable_repository_rugged } + + before do + repository.create_branch('search-files-by-content-branch', 'master') + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change') + new_commit_edit_new_file_on_branch(repository_rugged, 'anotherfile', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change') + end + + after do + ensure_seeds + end + + shared_examples 'search files by content' do + it 'should have 2 items' do + expect(search_results.size).to eq(2) + end + + it 'should have the correct matching line' do + expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n", + "search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n") + end + end + + it_should_behave_like 'search files by content' do + let(:search_results) do + repository.search_files_by_content('search-files-by-content', 'search-files-by-content-branch') + end + end + + it_should_behave_like 'search files by content' do + let(:search_results) do + repository.gitaly_repository_client.search_files_by_content( + 'search-files-by-content-branch', + 'search-files-by-content', + chunked_response: false + ) + end + end + end + describe '#find_remote_root_ref' do it 'gets the remote root ref from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::RemoteService) @@ -544,7 +588,7 @@ describe Gitlab::Git::Repository, :seed_helper do # Add new commits so that there's a renamed file in the commit history @commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid @rename_commit_id = new_commit_move_file(repository_rugged).oid - @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged).oid + @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged, "encoding/CHANGELOG", "Edit encoding/CHANGELOG", "I'm a new changelog with different text").oid end after do @@ -1230,7 +1274,7 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#gitattribute' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') } after do ensure_seeds @@ -1249,7 +1293,7 @@ describe Gitlab::Git::Repository, :seed_helper do end context 'without gitattributes file' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } it 'returns nil' do expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil) @@ -1513,7 +1557,7 @@ describe Gitlab::Git::Repository, :seed_helper do context 'repository does not exist' do it 'raises NoRepository and does not call Gitaly WriteConfig' do - repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '') + repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project') expect(repository.gitaly_repository_client).not_to receive(:write_config) @@ -1803,7 +1847,7 @@ describe Gitlab::Git::Repository, :seed_helper do out: '/dev/null', err: '/dev/null') - empty_repo = described_class.new('default', 'empty-repo.git', '') + empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo') expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' end @@ -1818,13 +1862,13 @@ describe Gitlab::Git::Repository, :seed_helper do File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) - non_valid = described_class.new('default', 'non-valid.git', '') + non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid') expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) end it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do - broken_repo = described_class.new('default', 'a/path.git', '') + broken_repo = described_class.new('default', 'a/path.git', '', 'a/path') expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) end @@ -1964,7 +2008,7 @@ describe Gitlab::Git::Repository, :seed_helper do end # Build the options hash that's passed to Rugged::Commit#create - def commit_options(repo, index, message) + def commit_options(repo, index, target, ref, message) options = {} options[:tree] = index.write_tree(repo) options[:author] = { @@ -1978,8 +2022,8 @@ describe Gitlab::Git::Repository, :seed_helper do time: Time.gm(2014, "mar", 3, 20, 15, 1) } options[:message] ||= message - options[:parents] = repo.empty? ? [] : [repo.head.target].compact - options[:update_ref] = "HEAD" + options[:parents] = repo.empty? ? [] : [target].compact + options[:update_ref] = ref options end @@ -1995,6 +2039,8 @@ describe Gitlab::Git::Repository, :seed_helper do options = commit_options( repo, index, + repo.head.target, + "HEAD", "Edit CHANGELOG in its original location" ) @@ -2003,19 +2049,24 @@ describe Gitlab::Git::Repository, :seed_helper do end # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of encoding/CHANGELOG with new text. - def new_commit_edit_new_file(repo) - oid = repo.write("I'm a new changelog with different text", :blob) + # contents of the specified file_path with new text. + def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) + oid = repo.write(text, :blob) index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) - - options = commit_options(repo, index, "Edit encoding/CHANGELOG") - + index.read_tree(branch.target.tree) + index.add(path: file_path, oid: oid, mode: 0100644) + options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end + # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the + # contents of encoding/CHANGELOG with new text. + def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) + branch = repo.branches[branch_name] + new_commit_edit_new_file(repo, file_path, commit_message, text, branch) + end + # Writes a new commit to the repo and returns a Rugged::Commit. Moves the # CHANGELOG file to the encoding/ directory. def new_commit_move_file(repo) @@ -2027,7 +2078,7 @@ describe Gitlab::Git::Repository, :seed_helper do index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) index.remove("CHANGELOG") - options = commit_options(repo, index, "Move CHANGELOG to encoding/") + options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") sha = Rugged::Commit.create(repo, options) repo.lookup(sha) diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index b51e3879f49..4c0291f64f0 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Tag, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } describe '#tags' do describe 'first tag' do diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index bec875fb03d..4a4d69490a3 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe Gitlab::Git::Tree, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } context :repo do let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb index aff47599ad6..d5508dbff5d 100644 --- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::GitalyClient::RemoteService do end describe '#fetch_internal_remote' do - let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + let(:remote_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '', 'group/project') } it 'sends an fetch_internal_remote message and returns the result value' do expect_any_instance_of(Gitaly::RemoteService::Stub) diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb index 550db6db6d9..78a5e195ad1 100644 --- a/spec/lib/gitlab/gitaly_client/util_spec.rb +++ b/spec/lib/gitlab/gitaly_client/util_spec.rb @@ -7,6 +7,7 @@ describe Gitlab::GitalyClient::Util do let(:gl_repository) { 'project-1' } let(:git_object_directory) { '.git/objects' } let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] } + let(:gl_project_path) { 'namespace/myproject' } let(:git_env) do { 'GIT_OBJECT_DIRECTORY_RELATIVE' => git_object_directory, @@ -15,7 +16,7 @@ describe Gitlab::GitalyClient::Util do end subject do - described_class.repository(repository_storage, relative_path, gl_repository) + described_class.repository(repository_storage, relative_path, gl_repository, gl_project_path) end it 'creates a Gitaly::Repository with the given data' do @@ -27,6 +28,7 @@ describe Gitlab::GitalyClient::Util do expect(subject.gl_repository).to eq(gl_repository) expect(subject.git_object_directory).to eq(git_object_directory) expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory) + expect(subject.gl_project_path).to eq(gl_project_path) end end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 77f5b2ffa37..47233ea6ee2 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -5,6 +5,14 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do let(:import_state) { double(:import_state) } let(:client) { double(:client) } + let(:wiki) do + double( + :wiki, + disk_path: 'foo.wiki', + full_path: 'group/foo.wiki' + ) + end + let(:project) do double( :project, @@ -15,7 +23,9 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do repository: repository, create_wiki: true, import_state: import_state, - lfs_enabled?: true + full_path: 'group/foo', + lfs_enabled?: true, + wiki: wiki ) end @@ -195,7 +205,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do it 'imports the wiki repository' do expect(importer.gitlab_shell) .to receive(:import_repository) - .with('foo', 'foo.wiki', 'foo.wiki.git') + .with('foo', 'foo.wiki', 'foo.wiki.git', 'group/foo.wiki') expect(importer.import_wiki_repository).to eq(true) end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index c7f92cbb143..8433d40b2ea 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -171,51 +171,6 @@ describe Gitlab::Kubernetes::Helm::Api do end end - describe '#update' do - let(:rbac) { false } - - let(:command) do - Gitlab::Kubernetes::Helm::UpgradeCommand.new( - application_name, - chart: 'chart-name', - files: files, - rbac: rbac - ) - end - - before do - allow(namespace).to receive(:ensure_exists!).once - - allow(client).to receive(:update_config_map).and_return(nil) - allow(client).to receive(:create_pod).and_return(nil) - allow(client).to receive(:delete_pod).and_return(nil) - end - - it 'ensures the namespace exists before creating the pod' do - expect(namespace).to receive(:ensure_exists!).once.ordered - expect(client).to receive(:create_pod).once.ordered - - subject.update(command) - end - - it 'removes an existing pod before updating' do - expect(client).to receive(:delete_pod).with('upgrade-app-name', 'gitlab-managed-apps').once.ordered - expect(client).to receive(:create_pod).once.ordered - - subject.update(command) - end - - it 'updates the config map on kubeclient when one exists' do - resource = Gitlab::Kubernetes::ConfigMap.new( - application_name, files - ).generate - - expect(client).to receive(:update_config_map).with(resource).once - - subject.update(command) - end - end - describe '#status' do let(:phase) { Gitlab::Kubernetes::Pod::RUNNING } let(:pod) { Kubeclient::Resource.new(status: { phase: phase }) } # partial representation diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 82ed4d47857..db76d5d207e 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -21,6 +21,15 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ) end + let(:tls_flags) do + <<~EOS.squish + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + EOS + end + subject { install_command } it_behaves_like 'helm commands' do @@ -36,12 +45,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_comand) do <<~EOS.squish - helm install chart-name - --name app-name - --tls - --tls-ca-cert /data/helm/app-name/config/ca.pem - --tls-cert /data/helm/app-name/config/cert.pem - --tls-key /data/helm/app-name/config/key.pem + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps @@ -66,12 +73,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_command) do <<~EOS.squish - helm install chart-name - --name app-name - --tls - --tls-ca-cert /data/helm/app-name/config/ca.pem - --tls-cert /data/helm/app-name/config/cert.pem - --tls-key /data/helm/app-name/config/key.pem + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} --version 1.2.3 --set rbac.create\\=true,rbac.enabled\\=true --namespace gitlab-managed-apps @@ -95,12 +100,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_command) do <<~EOS.squish - helm install chart-name - --name app-name - --tls - --tls-ca-cert /data/helm/app-name/config/ca.pem - --tls-cert /data/helm/app-name/config/cert.pem - --tls-key /data/helm/app-name/config/key.pem + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps @@ -120,15 +123,22 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done helm repo add app-name https://repository.example.com helm repo update + /bin/date + /bin/true #{helm_install_command} EOS end let(:helm_install_command) do - <<~EOS.strip - /bin/date - /bin/true - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml + <<~EOS.squish + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} + --version 1.2.3 + --set rbac.create\\=false,rbac.enabled\\=false + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml EOS end end @@ -145,14 +155,21 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do helm repo add app-name https://repository.example.com helm repo update #{helm_install_command} + /bin/date + /bin/false EOS end let(:helm_install_command) do - <<~EOS.strip - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml - /bin/date - /bin/false + <<~EOS.squish + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} + --version 1.2.3 + --set rbac.create\\=false,rbac.enabled\\=false + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml EOS end end @@ -174,8 +191,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_command) do <<~EOS.squish - helm install chart-name - --name app-name + helm upgrade app-name chart-name + --install + --reset-values --version 1.2.3 --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps @@ -201,12 +219,10 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do let(:helm_install_command) do <<~EOS.squish - helm install chart-name - --name app-name - --tls - --tls-ca-cert /data/helm/app-name/config/ca.pem - --tls-cert /data/helm/app-name/config/cert.pem - --tls-key /data/helm/app-name/config/key.pem + helm upgrade app-name chart-name + --install + --reset-values + #{tls_flags} --set rbac.create\\=false,rbac.enabled\\=false --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml diff --git a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb deleted file mode 100644 index 9b201dae417..00000000000 --- a/spec/lib/gitlab/kubernetes/helm/upgrade_command_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Gitlab::Kubernetes::Helm::UpgradeCommand do - let(:application) { build(:clusters_applications_prometheus) } - let(:files) { { 'ca.pem': 'some file content' } } - let(:namespace) { ::Gitlab::Kubernetes::Helm::NAMESPACE } - let(:rbac) { false } - let(:upgrade_command) do - described_class.new( - application.name, - chart: application.chart, - files: files, - rbac: rbac - ) - end - - subject { upgrade_command } - - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS - helm init --upgrade - for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done - helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml - EOS - end - end - - context 'rbac is true' do - let(:rbac) { true } - - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS - helm init --upgrade - for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done - helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml - EOS - end - end - end - - context 'with an application with a repository' do - let(:ci_runner) { create(:ci_runner) } - let(:application) { build(:clusters_applications_runner, runner: ci_runner) } - let(:upgrade_command) do - described_class.new( - application.name, - chart: application.chart, - files: files, - rbac: rbac, - repository: application.repository - ) - end - - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS - helm init --upgrade - for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done - helm repo add #{application.name} #{application.repository} - helm upgrade #{application.name} #{application.chart} --tls --tls-ca-cert /data/helm/#{application.name}/config/ca.pem --tls-cert /data/helm/#{application.name}/config/cert.pem --tls-key /data/helm/#{application.name}/config/key.pem --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml - EOS - end - end - end - - context 'when there is no ca.pem file' do - let(:files) { { 'file.txt': 'some content' } } - - it_behaves_like 'helm commands' do - let(:commands) do - <<~EOS - helm init --upgrade - for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done - helm upgrade #{application.name} #{application.chart} --reset-values --install --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml - EOS - end - end - end - - describe '#pod_resource' do - subject { upgrade_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end - end - - describe '#config_map_resource' do - let(:metadata) do - { - name: "values-content-configuration-#{application.name}", - namespace: namespace, - labels: { name: "values-content-configuration-#{application.name}" } - } - end - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } - - it 'returns a KubeClient resource with config map content for the application' do - expect(subject.config_map_resource).to eq(resource) - end - end - - describe '#rbac?' do - subject { upgrade_command.rbac? } - - context 'rbac is enabled' do - let(:rbac) { true } - - it { is_expected.to be_truthy } - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it { is_expected.to be_falsey } - end - end - - describe '#pod_name' do - it 'returns the pod name' do - expect(subject.pod_name).to eq("upgrade-#{application.name}") - end - end -end diff --git a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb index 7723533aee2..7519707293c 100644 --- a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb @@ -10,11 +10,17 @@ describe Gitlab::LegacyGithubImport::WikiFormatter do subject(:wiki) { described_class.new(project) } describe '#disk_path' do - it 'appends .wiki to project path' do + it 'appends .wiki to disk path' do expect(wiki.disk_path).to eq project.wiki.disk_path end end + describe '#full_path' do + it 'appends .wiki to project path' do + expect(wiki.full_path).to eq project.wiki.full_path + end + end + describe '#import_url' do it 'returns URL of the wiki repository' do expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git' diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb index 771b633a2b9..4b03f3c2532 100644 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do end it 'updates metrics type unix and with addr' do - labels = { type: 'unix', address: socket_address } + labels = { socket_type: 'unix', socket_address: socket_address } expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') @@ -69,7 +69,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do end it 'updates metrics type unix and with addr' do - labels = { type: 'tcp', address: tcp_socket_address } + labels = { socket_type: 'tcp', socket_address: tcp_socket_address } expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 57b0ef8d1ad..1cc2bde50e9 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -6,7 +6,12 @@ describe Gitlab::ProjectTemplate do expected = [ described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'), described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'), - described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express') + described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'), + described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'), + described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'), + described_class.new('plainhtml', 'Pages/Plain HTML', 'Everything you need to get started using a plain HTML Pages site.', 'https://gitlab.com/pages/plain-html'), + described_class.new('gitbook', 'Pages/GitBook', 'Everything you need to get started using a GitBook Pages site.', 'https://gitlab.com/pages/gitbook'), + described_class.new('hexo', 'Pages/Hexo', 'Everything you need to get started using a plan Hexo Pages site.', 'https://gitlab.com/pages/hexo') ] expect(described_class.all).to be_an(Array) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 6ce9d515a0f..033e1bf81a1 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -412,7 +412,7 @@ describe Gitlab::Shell do end it 'creates a repository' do - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_truthy + expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy expect(File.stat(created_path).mode & 0o777).to eq(0o770) @@ -427,7 +427,7 @@ describe Gitlab::Shell do # should cause #create_repository to fail. FileUtils.touch(created_path) - expect(gitlab_shell.create_repository(repository_storage, repo_name)).to be_falsy + expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_falsy end end @@ -474,13 +474,10 @@ describe Gitlab::Shell do end describe '#fork_repository' do + let(:target_project) { create(:project) } + subject do - gitlab_shell.fork_repository( - project.repository_storage, - project.disk_path, - 'nfs-file05', - 'fork/path' - ) + gitlab_shell.fork_repository(project, target_project) end it 'returns true when the command succeeds' do @@ -505,7 +502,7 @@ describe Gitlab::Shell do it 'returns true when the command succeeds' do expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository).with(import_url) - result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url, project.full_path) expect(result).to be_truthy end @@ -516,7 +513,7 @@ describe Gitlab::Shell do expect_any_instance_of(Gitlab::Shell::GitalyGitlabProjects).to receive(:output) { 'error'} expect do - gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url, project.full_path) end.to raise_error(Gitlab::Shell::Error, "error") end end diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb index 7876536cb3e..1a79f94cf0d 100644 --- a/spec/migrations/clean_up_for_members_spec.rb +++ b/spec/migrations/clean_up_for_members_spec.rb @@ -2,6 +2,10 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb') describe CleanUpForMembers, :migration do + before do + stub_feature_flags(enforced_sso: false) + end + let(:migration) { described_class.new } let(:groups) { table(:namespaces) } let!(:group_member) { create_group_member } diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb new file mode 100644 index 00000000000..b1ff3cfd355 --- /dev/null +++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb') + +describe MigrateAutoDevOpsDomainToClusterDomain, :migration do + include MigrationHelpers::ClusterHelpers + + let(:migration) { described_class.new } + let(:project_auto_devops_table) { table(:project_auto_devops) } + let(:clusters_table) { table(:clusters) } + let(:cluster_projects_table) { table(:cluster_projects) } + + # Following lets are needed by MigrationHelpers::ClusterHelpers + let(:cluster_kubernetes_namespaces_table) { table(:clusters_kubernetes_namespaces) } + let(:projects_table) { table(:projects) } + let(:namespaces_table) { table(:namespaces) } + let(:provider_gcp_table) { table(:cluster_providers_gcp) } + let(:platform_kubernetes_table) { table(:cluster_platforms_kubernetes) } + + before do + setup_cluster_projects_with_domain(quantity: 20, domain: domain) + end + + context 'with ProjectAutoDevOps with no domain' do + let(:domain) { nil } + + it 'should not update cluster project' do + migrate! + + expect(clusters_without_domain.count).to eq(clusters_table.count) + end + end + + context 'with ProjectAutoDevOps with domain' do + let(:domain) { 'example-domain.com' } + + it 'should update all cluster projects' do + migrate! + + expect(clusters_with_domain.count).to eq(clusters_table.count) + end + end + + context 'when only some ProjectAutoDevOps have domain set' do + let(:domain) { 'example-domain.com' } + + before do + setup_cluster_projects_with_domain(quantity: 25, domain: nil) + end + + it 'should only update specific cluster projects' do + migrate! + + expect(clusters_with_domain.count).to eq(20) + + project_auto_devops_with_domain.each do |project_auto_devops| + cluster_project = find_cluster_project(project_auto_devops.project_id) + cluster = find_cluster(cluster_project.cluster_id) + + expect(cluster.domain).to be_present + end + + expect(clusters_without_domain.count).to eq(25) + + project_auto_devops_without_domain.each do |project_auto_devops| + cluster_project = find_cluster_project(project_auto_devops.project_id) + cluster = find_cluster(cluster_project.cluster_id) + + expect(cluster.domain).not_to be_present + end + end + end + + def setup_cluster_projects_with_domain(quantity:, domain:) + create_cluster_project_list(quantity) + + cluster_projects = cluster_projects_table.last(quantity) + + cluster_projects.each do |cluster_project| + specific_domain = "#{cluster_project.id}-#{domain}" if domain + + project_auto_devops_table.create( + project_id: cluster_project.project_id, + enabled: true, + domain: specific_domain + ) + end + end + + def find_cluster_project(project_id) + cluster_projects_table.where(project_id: project_id).first + end + + def find_cluster(cluster_id) + clusters_table.where(id: cluster_id).first + end + + def project_auto_devops_with_domain + project_auto_devops_table.where.not("domain IS NULL OR domain = ''") + end + + def project_auto_devops_without_domain + project_auto_devops_table.where("domain IS NULL OR domain = ''") + end + + def clusters_with_domain + clusters_table.where.not("domain IS NULL OR domain = ''") + end + + def clusters_without_domain + clusters_table.where("domain IS NULL OR domain = ''") + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 96aa9a82b71..789e14e8a20 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -70,6 +70,13 @@ describe ApplicationSetting do .is_greater_than(0) end + it do + is_expected.to validate_numericality_of(:local_markdown_version) + .only_integer + .is_greater_than_or_equal_to(0) + .is_less_than(65536) + end + context 'key restrictions' do it 'supports all key types' do expect(described_class::SUPPORTED_KEY_TYPES).to contain_exactly(:rsa, :dsa, :ecdsa, :ed25519) diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb index 79a06c35459..cf5cbf8ec5c 100644 --- a/spec/models/clusters/applications/cert_manager_spec.rb +++ b/spec/models/clusters/applications/cert_manager_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::CertManager do include_examples 'cluster application core specs', :clusters_applications_cert_managers include_examples 'cluster application status specs', :clusters_applications_cert_managers + include_examples 'cluster application version specs', :clusters_applications_cert_managers include_examples 'cluster application initial status specs' describe '#install_command' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 6d48131d1cc..03ca18c6943 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -7,6 +7,7 @@ describe Clusters::Applications::Ingress do include_examples 'cluster application core specs', :clusters_applications_ingress include_examples 'cluster application status specs', :clusters_applications_ingress + include_examples 'cluster application version specs', :clusters_applications_ingress include_examples 'cluster application helm specs', :clusters_applications_ingress include_examples 'cluster application initial status specs' diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index b73a243f6e0..2c22c24c498 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' describe Clusters::Applications::Jupyter do include_examples 'cluster application core specs', :clusters_applications_jupyter include_examples 'cluster application status specs', :clusters_applications_jupyter + include_examples 'cluster application version specs', :clusters_applications_jupyter include_examples 'cluster application helm specs', :clusters_applications_jupyter it { is_expected.to belong_to(:oauth_application) } diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 5519615d52d..cd29e0d4f53 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -9,6 +9,7 @@ describe Clusters::Applications::Knative do include_examples 'cluster application core specs', :clusters_applications_knative include_examples 'cluster application status specs', :clusters_applications_knative include_examples 'cluster application helm specs', :clusters_applications_knative + include_examples 'cluster application version specs', :clusters_applications_knative include_examples 'cluster application initial status specs' before do diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 073fbded8ac..caf59b0fc31 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :clusters_applications_prometheus + include_examples 'cluster application version specs', :clusters_applications_prometheus include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' @@ -206,8 +207,8 @@ describe Clusters::Applications::Prometheus do let(:prometheus) { build(:clusters_applications_prometheus) } let(:values) { prometheus.values } - it 'returns an instance of Gitlab::Kubernetes::Helm::GetCommand' do - expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::UpgradeCommand) + it 'returns an instance of Gitlab::Kubernetes::Helm::InstallCommand' do + expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand) end it 'should be initialized with 3 arguments' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 96b7b02dbaf..38758ff97bc 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do include_examples 'cluster application core specs', :clusters_applications_runner include_examples 'cluster application status specs', :clusters_applications_runner + include_examples 'cluster application version specs', :clusters_applications_runner include_examples 'cluster application helm specs', :clusters_applications_runner include_examples 'cluster application initial status specs' diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 0161db740ee..92ce2b0999a 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -30,6 +30,7 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix } it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix } + it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix } it { is_expected.to respond_to :project } @@ -514,4 +515,108 @@ describe Clusters::Cluster do it { is_expected.to be_falsey } end end + + describe '#kube_ingress_domain' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { cluster.kube_ingress_domain } + + context 'with domain set in cluster' do + let(:cluster) { create(:cluster, :provided_by_gcp, :with_domain) } + + it { is_expected.to eq(cluster.domain) } + end + + context 'with no domain on cluster' do + context 'with a project cluster' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:project) { cluster.project } + + context 'with domain set at instance level' do + before do + stub_application_setting(auto_devops_domain: 'global_domain.com') + + it { is_expected.to eq('global_domain.com') } + end + end + + context 'with domain set on ProjectAutoDevops' do + before do + auto_devops = project.build_auto_devops(domain: 'legacy-ado-domain.com') + auto_devops.save + end + + it { is_expected.to eq('legacy-ado-domain.com') } + end + + context 'with domain set as environment variable on project' do + before do + variable = project.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'project-ado-domain.com') + variable.save + end + + it { is_expected.to eq('project-ado-domain.com') } + end + + context 'with domain set as environment variable on the group project' do + let(:group) { create(:group) } + + before do + project.update(parent_id: group.id) + variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com') + variable.save + end + + it { is_expected.to eq('group-ado-domain.com') } + end + end + + context 'with a group cluster' do + let(:cluster) { create(:cluster, :group, :provided_by_gcp) } + + context 'with domain set as environment variable for the group' do + let(:group) { cluster.group } + + before do + variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com') + variable.save + end + + it { is_expected.to eq('group-ado-domain.com') } + end + end + end + end + + describe '#predefined_variables' do + subject { cluster.predefined_variables } + + context 'with an instance domain' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + before do + stub_application_setting(auto_devops_domain: 'global_domain.com') + end + + it 'should include KUBE_INGRESS_BASE_DOMAIN' do + expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'global_domain.com') + end + end + + context 'with a cluster domain' do + let(:cluster) { create(:cluster, :provided_by_gcp, domain: 'example.com') } + + it 'should include KUBE_INGRESS_BASE_DOMAIN' do + expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'example.com') + end + end + + context 'with no domain' do + let(:cluster) { create(:cluster, :provided_by_gcp, :project) } + + it 'should return an empty array' do + expect(subject.to_hash).to be_empty + end + end + end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 6c8a223092e..c273fa7e164 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -297,6 +297,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end end + + context 'with a domain' do + let!(:cluster) do + create(:cluster, :provided_by_gcp, :with_domain, + platform_kubernetes: kubernetes) + end + + it 'sets KUBE_INGRESS_BASE_DOMAIN' do + expect(subject).to include( + { key: 'KUBE_INGRESS_BASE_DOMAIN', value: cluster.domain, public: true } + ) + end + end end describe '#terminals' do diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 29197ef372e..447279f19a8 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -60,6 +60,10 @@ describe CacheMarkdownField do changes_applied end end + + def has_attribute?(attr_name) + attribute_names.include?(attr_name) + end end def thing_subclass(new_attr) @@ -72,8 +76,8 @@ describe CacheMarkdownField do let(:updated_markdown) { '`Bar`' } let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' } - let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } - let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION } + let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) } + let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 } before do stub_commonmark_sourcepos_disabled @@ -94,11 +98,11 @@ describe CacheMarkdownField do it { expect(thing.foo).to eq(markdown) } it { expect(thing.foo_html).to eq(html) } it { expect(thing.foo_html_changed?).not_to be_truthy } - it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) } + it { expect(thing.cached_markdown_version).to eq(cache_version) } end context 'a changed markdown field' do - let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) } + let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) } before do thing.foo = updated_markdown @@ -139,7 +143,7 @@ describe CacheMarkdownField do end context 'a non-markdown field changed' do - let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) } + let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) } before do thing.bar = 'OK' @@ -160,7 +164,7 @@ describe CacheMarkdownField do end it { expect(thing.foo_html).to eq(updated_html) } - it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) } + it { expect(thing.cached_markdown_version).to eq(cache_version) } end describe '#cached_html_up_to_date?' do @@ -174,21 +178,35 @@ describe CacheMarkdownField do is_expected.to be_falsy end - it 'returns false when the version is too early' do - thing.cached_markdown_version -= 1 + it 'returns false when the cached version is too old' do + thing.cached_markdown_version = cache_version - 1 is_expected.to be_falsy end - it 'returns false when the version is too late' do - thing.cached_markdown_version += 1 + it 'returns false when the cached version is in future' do + thing.cached_markdown_version = cache_version + 1 is_expected.to be_falsy end - it 'returns true when the version is just right' do + it 'returns false when the local version was bumped' do + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2) thing.cached_markdown_version = cache_version + is_expected.to be_falsy + end + + it 'returns true when the local version is default' do + thing.cached_markdown_version = cache_version + + is_expected.to be_truthy + end + + it 'returns true when the cached version is just right' do + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2) + thing.cached_markdown_version = cache_version + 2 + is_expected.to be_truthy end @@ -221,14 +239,9 @@ describe CacheMarkdownField do describe '#latest_cached_markdown_version' do subject { thing.latest_cached_markdown_version } - it 'returns commonmark version' do - thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1 - is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) - end - - it 'returns default version when version is nil' do + it 'returns default version' do thing.cached_markdown_version = nil - is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) + is_expected.to eq(cache_version) end end @@ -255,7 +268,7 @@ describe CacheMarkdownField do thing.cached_markdown_version = nil thing.refresh_markdown_cache - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) + expect(thing.cached_markdown_version).to eq(cache_version) end end @@ -336,7 +349,7 @@ describe CacheMarkdownField do expect(thing.foo_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html) - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) + expect(thing.cached_markdown_version).to eq(cache_version) end end @@ -356,7 +369,7 @@ describe CacheMarkdownField do expect(thing.foo_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html) - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) + expect(thing.cached_markdown_version).to eq(cache_version) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ae137aa7b78..c1767ed0535 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1765,7 +1765,7 @@ describe Project do context 'using a regular repository' do it 'creates the repository' do expect(shell).to receive(:create_repository) - .with(project.repository_storage, project.disk_path) + .with(project.repository_storage, project.disk_path, project.full_path) .and_return(true) expect(project.repository).to receive(:after_create) @@ -1775,7 +1775,7 @@ describe Project do it 'adds an error if the repository could not be created' do expect(shell).to receive(:create_repository) - .with(project.repository_storage, project.disk_path) + .with(project.repository_storage, project.disk_path, project.full_path) .and_return(false) expect(project.repository).not_to receive(:after_create) @@ -1808,7 +1808,7 @@ describe Project do .and_return(false) allow(shell).to receive(:create_repository) - .with(project.repository_storage, project.disk_path) + .with(project.repository_storage, project.disk_path, project.full_path) .and_return(true) expect(project).to receive(:create_repository).with(force: true) @@ -1832,7 +1832,7 @@ describe Project do .and_return(false) expect(shell).to receive(:create_repository) - .with(project.repository_storage, project.disk_path) + .with(project.repository_storage, project.disk_path, project.full_path) .and_return(true) project.ensure_repository diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 48a43801b9f..3ccc706edf2 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -7,7 +7,7 @@ describe ProjectWiki do let(:repository) { project.repository } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_wiki) { described_class.new(project, user) } - let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') } + let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo', 'group/project.wiki') } let(:commit) { project_wiki.repository.head_commit } subject { project_wiki } @@ -75,7 +75,7 @@ describe ProjectWiki do # Create a fresh project which will not have a wiki project_wiki = described_class.new(create(:project), user) gitlab_shell = double(:gitlab_shell) - allow(gitlab_shell).to receive(:create_repository) + allow(gitlab_shell).to receive(:create_wiki_repository) allow(project_wiki).to receive(:gitlab_shell).and_return(gitlab_shell) expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 4978c43c9b5..f78760bf567 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2291,6 +2291,7 @@ describe Repository do expect(subject).to be_a(Gitlab::Git::Repository) expect(subject.relative_path).to eq(project.disk_path + '.git') expect(subject.gl_repository).to eq("project-#{project.id}") + expect(subject.gl_project_path).to eq(project.full_path) end context 'with a wiki repository' do @@ -2300,6 +2301,7 @@ describe Repository do expect(subject).to be_a(Gitlab::Git::Repository) expect(subject.relative_path).to eq(project.disk_path + '.wiki.git') expect(subject.gl_repository).to eq("wiki-#{project.id}") + expect(subject.gl_project_path).to eq(project.full_path) end end end diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb index 3797960ac3d..7eeb2fae57d 100644 --- a/spec/models/resource_label_event_spec.rb +++ b/spec/models/resource_label_event_spec.rb @@ -81,14 +81,14 @@ RSpec.describe ResourceLabelEvent, type: :model do expect(subject.outdated_markdown?).to be true end - it 'returns true markdown is outdated' do - subject.attributes = { cached_markdown_version: 0 } + it 'returns true if markdown is outdated' do + subject.attributes = { cached_markdown_version: ((CacheMarkdownField::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 } expect(subject.outdated_markdown?).to be true end it 'returns false if label and reference are set' do - subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION } + subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 } expect(subject.outdated_markdown?).to be false end diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index e85e7a41017..bb1db9a3d51 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe BlobPresenter, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } let(:git_blob) do Gitlab::Git::Blob.find( diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb new file mode 100644 index 00000000000..3769f8b78e4 --- /dev/null +++ b/spec/requests/api/group_labels_spec.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::GroupLabels do + let(:user) { create(:user) } + let(:group) { create(:group) } + let!(:group_member) { create(:group_member, group: group, user: user) } + let!(:label1) { create(:group_label, title: 'feature', group: group) } + let!(:label2) { create(:group_label, title: 'bug', group: group) } + + describe 'GET :id/labels' do + it 'returns all available labels for the group' do + get api("/groups/#{group.id}/labels", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/group_labels') + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug') + end + end + + describe 'POST /groups/:id/labels' do + it 'returns created label when all params are given' do + post api("/groups/#{group.id}/labels", user), + params: { + name: 'Foo', + color: '#FFAABB', + description: 'test' + } + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq('Foo') + expect(json_response['color']).to eq('#FFAABB') + expect(json_response['description']).to eq('test') + end + + it 'returns created label when only required params are given' do + post api("/groups/#{group.id}/labels", user), + params: { + name: 'Foo & Bar', + color: '#FFAABB' + } + + expect(response.status).to eq(201) + expect(json_response['name']).to eq('Foo & Bar') + expect(json_response['color']).to eq('#FFAABB') + expect(json_response['description']).to be_nil + end + + it 'returns a 400 bad request if name not given' do + post api("/groups/#{group.id}/labels", user), params: { color: '#FFAABB' } + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns a 400 bad request if color is not given' do + post api("/groups/#{group.id}/labels", user), params: { name: 'Foobar' } + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 409 if label already exists' do + post api("/groups/#{group.id}/labels", user), + params: { + name: label1.name, + color: '#FFAABB' + } + + expect(response).to have_gitlab_http_status(409) + expect(json_response['message']).to eq('Label already exists') + end + end + + describe 'DELETE /groups/:id/labels' do + it 'returns 204 for existing label' do + delete api("/groups/#{group.id}/labels", user), params: { name: label1.name } + + expect(response).to have_gitlab_http_status(204) + end + + it 'returns 404 for non existing label' do + delete api("/groups/#{group.id}/labels", user), params: { name: 'label2' } + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Label Not Found') + end + + it 'returns 400 for wrong parameters' do + delete api("/groups/#{group.id}/labels", user) + + expect(response).to have_gitlab_http_status(400) + end + + it "does not delete parent's group labels", :nested_groups do + subgroup = create(:group, parent: group) + subgroup_label = create(:group_label, title: 'feature', group: subgroup) + + delete api("/groups/#{subgroup.id}/labels", user), params: { name: subgroup_label.name } + + expect(response).to have_gitlab_http_status(204) + expect(subgroup.labels.size).to eq(0) + expect(group.labels).to include(label1) + end + + it_behaves_like '412 response' do + let(:request) { api("/groups/#{group.id}/labels", user) } + let(:params) { { name: label1.name } } + end + end + + describe 'PUT /groups/:id/labels' do + it 'returns 200 if name and colors and description are changed' do + put api("/groups/#{group.id}/labels", user), + params: { + name: label1.name, + new_name: 'New Label', + color: '#FFFFFF', + description: 'test' + } + + expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['description']).to eq('test') + end + + it "does not update parent's group label", :nested_groups do + subgroup = create(:group, parent: group) + subgroup_label = create(:group_label, title: 'feature', group: subgroup) + + put api("/groups/#{subgroup.id}/labels", user), + params: { + name: subgroup_label.name, + new_name: 'New Label' + } + + expect(response).to have_gitlab_http_status(200) + expect(subgroup.labels[0].name).to eq('New Label') + expect(label1.name).to eq('feature') + end + + it 'returns 404 if label does not exist' do + put api("/groups/#{group.id}/labels", user), + params: { + name: 'label2', + new_name: 'label3' + } + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 400 if no label name given' do + put api("/groups/#{group.id}/labels", user), params: { new_name: label1.name } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('name is missing') + end + + it 'returns 400 if no new parameters given' do + put api("/groups/#{group.id}/labels", user), params: { name: label1.name } + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('new_name, color, description are missing, '\ + 'at least one parameter must be provided') + end + end + + describe 'POST /groups/:id/labels/:label_id/subscribe' do + context 'when label_id is a label title' do + it 'subscribes to the label' do + post api("/groups/#{group.id}/labels/#{label1.title}/subscribe", user) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(label1.title) + expect(json_response['subscribed']).to be_truthy + end + end + + context 'when label_id is a label ID' do + it 'subscribes to the label' do + post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(label1.title) + expect(json_response['subscribed']).to be_truthy + end + end + + context 'when user is already subscribed to label' do + before do + label1.subscribe(user) + end + + it 'returns 304' do + post api("/groups/#{group.id}/labels/#{label1.id}/subscribe", user) + + expect(response).to have_gitlab_http_status(304) + end + end + + context 'when label ID is not found' do + it 'returns 404 error' do + post api("/groups/#{group.id}/labels/1234/subscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe 'POST /groups/:id/labels/:label_id/unsubscribe' do + before do + label1.subscribe(user) + end + + context 'when label_id is a label title' do + it 'unsubscribes from the label' do + post api("/groups/#{group.id}/labels/#{label1.title}/unsubscribe", user) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(label1.title) + expect(json_response['subscribed']).to be_falsey + end + end + + context 'when label_id is a label ID' do + it 'unsubscribes from the label' do + post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user) + + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(label1.title) + expect(json_response['subscribed']).to be_falsey + end + end + + context 'when user is already unsubscribed from label' do + before do + label1.unsubscribe(user) + end + + it 'returns 304' do + post api("/groups/#{group.id}/labels/#{label1.id}/unsubscribe", user) + + expect(response).to have_gitlab_http_status(304) + end + end + + context 'when label ID is not found' do + it 'returns 404 error' do + post api("/groups/#{group.id}/labels/1234/unsubscribe", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 51343287a13..0f5f6e38819 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -951,6 +951,29 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(404) end + + describe "the squash_commit_message param" do + let(:squash_commit) do + project.repository.commits_between(json_response['diff_refs']['start_sha'], json_response['merge_commit_sha']).first + end + + it "results in a specific squash commit message when set" do + squash_commit_message = 'My custom squash commit message' + + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { + squash: true, + squash_commit_message: squash_commit_message + } + + expect(squash_commit.message.chomp).to eq(squash_commit_message) + end + + it "results in a default squash commit message when not set" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { squash: true } + + expect(squash_commit.message).to eq(merge_request.default_squash_commit_message) + end + end end describe "PUT /projects/:id/merge_requests/:merge_request_iid" do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 45fb1562e84..f33eb5b9e02 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -64,7 +64,8 @@ describe API::Settings, 'Settings' do performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, diff_max_patch_bytes: 150_000, - default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE + default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE, + local_markdown_version: 3 } expect(response).to have_gitlab_http_status(200) @@ -90,6 +91,7 @@ describe API::Settings, 'Settings' do expect(json_response['instance_statistics_visibility_private']).to be(true) expect(json_response['diff_max_patch_bytes']).to eq(150_000) expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + expect(json_response['local_markdown_version']).to eq(3) end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 2b148c1b563..2a455523e2c 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -35,7 +35,7 @@ describe 'OpenID Connect requests' do 'name' => 'Alice', 'nickname' => 'alice', 'email' => 'public@example.com', - 'email_verified' => true, + 'email_verified' => false, 'website' => 'https://example.com', 'profile' => 'http://localhost/alice', 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", @@ -111,6 +111,18 @@ describe 'OpenID Connect requests' do it 'does not include any unknown claims' do expect(json_response.keys).to eq %w[sub sub_legacy] + user_info_claims.keys end + + it 'includes email and email_verified claims' do + expect(json_response.keys).to include('email', 'email_verified') + end + + it 'has public email in email claim' do + expect(json_response['email']).to eq(user.public_email) + end + + it 'has false in email_verified claim' do + expect(json_response['email_verified']).to eq(false) + end end context 'ID token payload' do @@ -175,7 +187,35 @@ describe 'OpenID Connect requests' do expect(response).to have_gitlab_http_status(200) expect(json_response['issuer']).to eq('http://localhost') expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') - expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid]) + expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid profile email]) + end + end + + context 'Application with OpenID and email scopes' do + let(:application) { create :oauth_application, scopes: 'openid email' } + + it 'token response includes an ID token' do + request_access_token! + + expect(json_response).to include 'id_token' + end + + context 'UserInfo payload' do + before do + request_user_info! + end + + it 'includes the email and email_verified claims' do + expect(json_response.keys).to include('email', 'email_verified') + end + + it 'has private email in email claim' do + expect(json_response['email']).to eq(user.email) + end + + it 'has true in email_verified claim' do + expect(json_response['email_verified']).to eq(true) + end end end end diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index 88d16a5b360..7e151c3744e 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -21,6 +21,14 @@ describe ClusterApplicationEntity do expect(subject[:status_reason]).to be_nil end + context 'non-helm application' do + let(:application) { build(:clusters_applications_runner, version: '0.0.0') } + + it 'has update_available' do + expect(subject[:update_available]).to be_truthy + end + end + context 'when application is errored' do let(:application) { build(:clusters_applications_helm, :errored) } diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index cfa5414b40f..894fd7a0a12 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -1,14 +1,22 @@ require 'spec_helper' describe DeploymentEntity do - let(:user) { create(:user) } + let(:user) { developer } + let(:developer) { create(:user) } + let(:reporter) { create(:user) } + let(:project) { create(:project) } let(:request) { double('request') } - let(:deployment) { create(:deployment) } + let(:deployment) { create(:deployment, deployable: build, project: project) } + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + let(:pipeline) { create(:ci_pipeline, project: project, user: user) } let(:entity) { described_class.new(deployment, request: request) } subject { entity.as_json } before do + project.add_developer(developer) + project.add_reporter(reporter) allow(request).to receive(:current_user).and_return(user) + allow(request).to receive(:project).and_return(project) end it 'exposes internal deployment id' do @@ -23,6 +31,24 @@ describe DeploymentEntity do expect(subject).to include(:created_at) end + context 'when the pipeline has another manual action' do + let(:other_build) { create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline) } + let!(:other_deployment) { create(:deployment, deployable: other_build) } + + it 'returns another manual action' do + expect(subject[:manual_actions].count).to eq(1) + expect(subject[:manual_actions].first[:name]).to eq('another deploy') + end + + context 'when user is a reporter' do + let(:user) { reporter } + + it 'returns another manual action' do + expect(subject[:manual_actions]).not_to be_present + end + end + end + describe 'scheduled_actions' do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_pipeline, project: project, user: user) } diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 87493a28d1f..3541bd5f12e 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -10,6 +10,10 @@ describe EnvironmentSerializer do .represent(resource) end + before do + project.add_developer(user) + end + context 'when there is a single object provided' do let(:project) { create(:project, :repository) } let(:deployable) { create(:ci_build) } diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb index 45b8ce94815..19446ce1cf8 100644 --- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb +++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Clusters::Applications::CheckInstallationProgressService do +describe Clusters::Applications::CheckInstallationProgressService, '#execute' do RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze let(:application) { create(:clusters_applications_helm, :installing) } @@ -21,24 +21,39 @@ describe Clusters::Applications::CheckInstallationProgressService do expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once expect(service).not_to receive(:remove_installation_pod) - service.execute + expect do + service.execute + + application.reload + end.not_to change(application, :status) - expect(application).to be_installing expect(application.status_reason).to be_nil end end + end + end - context 'when timeouted' do - let(:application) { create(:clusters_applications_helm, :timeouted) } + shared_examples 'error logging' do + context 'when installation raises a Kubeclient::HttpError' do + let(:cluster) { create(:cluster, :provided_by_user, :project) } - it 'make the application errored' do - expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) + before do + application.update!(cluster: cluster) - service.execute + expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end - expect(application).to be_errored - expect(application.status_reason).to eq("Installation timed out. Check pod logs for install-helm for more details.") - end + it 'shows the response code from the error' do + service.execute + + expect(application).to be_errored.or(be_update_errored) + expect(application.status_reason).to eq('Kubernetes error: 401') + end + + it 'should log error' do + expect(service.send(:logger)).to receive(:error) + + service.execute end end end @@ -48,10 +63,15 @@ describe Clusters::Applications::CheckInstallationProgressService do allow(service).to receive(:remove_installation_pod).and_return(nil) end - describe '#execute' do + context 'when application is updating' do + let(:application) { create(:clusters_applications_helm, :updating) } + + include_examples 'error logging' + + RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } + context 'when installation POD succeeded' do let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } - before do expect(service).to receive(:installation_phase).once.and_return(phase) end @@ -67,7 +87,7 @@ describe Clusters::Applications::CheckInstallationProgressService do service.execute - expect(application).to be_installed + expect(application).to be_updated expect(application.status_reason).to be_nil end end @@ -83,33 +103,86 @@ describe Clusters::Applications::CheckInstallationProgressService do it 'make the application errored' do service.execute - expect(application).to be_errored - expect(application.status_reason).to eq("Installation failed. Check pod logs for install-helm for more details.") + expect(application).to be_update_errored + expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.') end end - RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } + context 'when timed out' do + let(:application) { create(:clusters_applications_helm, :timeouted, :updating) } - context 'when installation raises a Kubeclient::HttpError' do - let(:cluster) { create(:cluster, :provided_by_user, :project) } + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do + expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) + + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.') + end + end + end + + context 'when application is installing' do + include_examples 'error logging' + + RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase } + context 'when installation POD succeeded' do + let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED } before do - application.update!(cluster: cluster) + expect(service).to receive(:installation_phase).once.and_return(phase) + end - expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + it 'removes the installation POD' do + expect(service).to receive(:remove_installation_pod).once + + service.execute end - it 'shows the response code from the error' do + it 'make the application installed' do + expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) + + service.execute + + expect(application).to be_installed + expect(application.status_reason).to be_nil + end + end + + context 'when installation POD failed' do + let(:phase) { Gitlab::Kubernetes::Pod::FAILED } + let(:errors) { 'test installation failed' } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) + end + + it 'make the application errored' do service.execute expect(application).to be_errored - expect(application.status_reason).to eq('Kubernetes error: 401') + expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.') + end + end + + context 'when timed out' do + let(:application) { create(:clusters_applications_helm, :timeouted) } + + before do + expect(service).to receive(:installation_phase).once.and_return(phase) end - it 'should log error' do - expect(service.send(:logger)).to receive(:error) + it 'make the application errored' do + expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in) service.execute + + expect(application).to be_errored + expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.') end end end diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb index 1a2ca23748a..3f621ed5944 100644 --- a/spec/services/clusters/applications/create_service_spec.rb +++ b/spec/services/clusters/applications/create_service_spec.rb @@ -13,6 +13,7 @@ describe Clusters::Applications::CreateService do describe '#execute' do before do allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterUpgradeAppWorker).to receive(:perform_async) end subject { service.execute(test_request) } @@ -31,6 +32,22 @@ describe Clusters::Applications::CreateService do subject end + context 'application already installed' do + let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) } + + it 'does not create a new application' do + expect do + subject + end.not_to change(Clusters::Applications::Helm, :count) + end + + it 'schedules an upgrade for the application' do + expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original + + subject + end + end + context 'cert manager application' do let(:params) do { diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb index 21797edd533..8380932dfaa 100644 --- a/spec/services/clusters/applications/schedule_installation_service_spec.rb +++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb @@ -49,5 +49,29 @@ describe Clusters::Applications::ScheduleInstallationService do it_behaves_like 'a failing service' end + + context 'when application is installed' do + let(:application) { create(:clusters_applications_helm, :installed) } + + it 'schedules an upgrade via worker' do + expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once + + service.execute + + expect(application).to be_scheduled + end + end + + context 'when application is updated' do + let(:application) { create(:clusters_applications_helm, :updated) } + + it 'schedules an upgrade via worker' do + expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once + + service.execute + + expect(application).to be_scheduled + end + end end end diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb new file mode 100644 index 00000000000..1822fc38dbd --- /dev/null +++ b/spec/services/clusters/applications/upgrade_service_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Applications::UpgradeService do + describe '#execute' do + let(:application) { create(:clusters_applications_helm, :scheduled) } + let!(:install_command) { application.install_command } + let(:service) { described_class.new(application) } + let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) } + + before do + allow(service).to receive(:install_command).and_return(install_command) + allow(service).to receive(:helm_api).and_return(helm_client) + end + + context 'when there are no errors' do + before do + expect(helm_client).to receive(:update).with(install_command) + allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil) + end + + it 'make the application updating' do + expect(application.cluster).not_to be_nil + service.execute + + expect(application).to be_updating + end + + it 'schedule async installation status check' do + expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once + + service.execute + end + end + + context 'when kubernetes cluster communication fails' do + let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) } + + before do + expect(helm_client).to receive(:update).with(install_command).and_raise(error) + end + + it 'make the application errored' do + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to match('Kubernetes error: 500') + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::UpgradeService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'Kubeclient::HttpError', + message: 'system failure', + service: 'Clusters::Applications::UpgradeService', + app_id: application.id, + project_ids: application.cluster.project_ids, + group_ids: [], + error_code: 500 + } + ) + + service.execute + end + end + + context 'a non kubernetes error happens' do + let(:application) { create(:clusters_applications_helm, :scheduled) } + let(:error) { StandardError.new('something bad happened') } + + before do + expect(application).to receive(:make_updating!).once.and_raise(error) + end + + it 'make the application errored' do + expect(helm_client).not_to receive(:update) + + service.execute + + expect(application).to be_update_errored + expect(application.status_reason).to eq("Can't start upgrade process.") + end + + it 'logs errors' do + expect(service.send(:logger)).to receive(:error).with( + { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::UpgradeService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with( + error, + extra: { + exception: 'StandardError', + error_code: nil, + message: 'something bad happened', + service: 'Clusters::Applications::UpgradeService', + app_id: application.id, + project_ids: application.cluster.projects.pluck(:id), + group_ids: [] + } + ) + + service.execute + end + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index ef76e2311b1..931e47d3a77 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -471,6 +471,8 @@ describe Issues::UpdateService, :mailer do it { expect(issue.tasks?).to eq(true) } + it_behaves_like 'updating a single task' + context 'when tasks are marked as completed' do before do update_issue(description: "- [x] Task 1\n- [X] Task 2") @@ -543,76 +545,6 @@ describe Issues::UpdateService, :mailer do end end - context 'when updating a single task' do - before do - update_issue(description: "- [ ] Task 1\n- [ ] Task 2") - end - - it { expect(issue.tasks?).to eq(true) } - - context 'when a task is marked as completed' do - before do - update_issue(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 }) - end - - it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as completed') - - expect(note1).not_to be_nil - - description_notes = find_notes('description') - expect(description_notes.length).to eq(1) - end - end - - context 'when a task is marked as incomplete' do - before do - update_issue(description: "- [x] Task 1\n- [X] Task 2") - update_issue(update_task: { index: 2, checked: false, line_source: '- [X] Task 2', line_number: 2 }) - end - - it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 2** as incomplete') - - expect(note1).not_to be_nil - - description_notes = find_notes('description') - expect(description_notes.length).to eq(1) - end - end - - context 'when the task position has been modified' do - before do - update_issue(description: "- [ ] Task 1\n- [ ] Task 3\n- [ ] Task 2") - end - - it 'raises an exception' do - expect(Note.count).to eq(2) - expect do - update_issue(update_task: { index: 2, checked: true, line_source: '- [ ] Task 2', line_number: 2 }) - end.to raise_error(ActiveRecord::StaleObjectError) - expect(Note.count).to eq(2) - end - end - - context 'when the content changes but not task line number' do - before do - update_issue(description: "Paragraph\n\n- [ ] Task 1\n- [x] Task 2") - update_issue(description: "Paragraph with more words\n\n- [ ] Task 1\n- [x] Task 2") - update_issue(update_task: { index: 2, checked: false, line_source: '- [x] Task 2', line_number: 4 }) - end - - it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 2** as incomplete') - - expect(note1).not_to be_nil - - description_notes = find_notes('description') - expect(description_notes.length).to eq(2) - end - end - end - context 'updating labels' do let(:label3) { create(:label, project: project) } let(:result) { described_class.new(project, user, params).execute(issue).reload } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index be5ad849ba7..20580bf14b9 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -466,6 +466,8 @@ describe MergeRequests::UpdateService, :mailer do it { expect(@merge_request.tasks?).to eq(true) } + it_behaves_like 'updating a single task' + context 'when tasks are marked as completed' do before do update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 54ce33dd103..d1b110b9806 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -116,7 +116,7 @@ describe Projects::CreateService, '#execute' do def wiki_repo(project) relative_path = ProjectWiki.new(project).disk_path + '.git' - Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar') + Gitlab::Git::Repository.new(project.repository_storage, relative_path, 'foobar', project.full_path) end end @@ -198,7 +198,7 @@ describe Projects::CreateService, '#execute' do context 'with legacy storage' do before do - gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing") + gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing", 'group/project') end after do @@ -234,7 +234,7 @@ describe Projects::CreateService, '#execute' do end before do - gitlab_shell.create_repository(repository_storage, hashed_path) + gitlab_shell.create_repository(repository_storage, hashed_path, 'group/project') end after do diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 26e8d829345..23ec29cce7b 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -119,7 +119,7 @@ describe Projects::ForkService do let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path } before do - gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") + gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}", "#{@to_user.namespace.full_path}/#{@from_project.path}") end after do diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 766276fdba3..aae50d5307f 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -201,7 +201,7 @@ describe Projects::TransferService do before do group.add_owner(user) - unless gitlab_shell.create_repository(repository_storage, "#{group.full_path}/#{project.path}") + unless gitlab_shell.create_repository(repository_storage, "#{group.full_path}/#{project.path}", project.full_path) raise 'failed to add repository' end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 8adfc63222e..90eaea9c872 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -232,7 +232,7 @@ describe Projects::UpdateService do let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) } before do - gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing") + gitlab_shell.create_repository(repository_storage, "#{user.namespace.full_path}/existing", user.namespace.full_path) end after do diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb index cc64dd25085..7c5480d382f 100644 --- a/spec/services/task_list_toggle_service_spec.rb +++ b/spec/services/task_list_toggle_service_spec.rb @@ -67,6 +67,17 @@ describe TaskListToggleService do expect(toggler.execute).to be_falsey end + it 'tolerates \r\n line endings' do + rn_markdown = markdown.gsub("\n", "\r\n") + toggler = described_class.new(rn_markdown, markdown_html, + toggle_as_checked: true, + line_source: '* [ ] Task 1', line_number: 1) + + expect(toggler.execute).to be_truthy + expect(toggler.updated_markdown.lines[0]).to eq "* [x] Task 1\r\n" + expect(toggler.updated_markdown_html).to include('disabled checked> Task 1') + end + it 'returns false if markdown is nil' do toggler = described_class.new(nil, markdown_html, toggle_as_checked: false, diff --git a/spec/support/shared_examples/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb index 42f3b4db23c..c3d40c5b231 100644 --- a/spec/support/shared_examples/issuable_shared_examples.rb +++ b/spec/support/shared_examples/issuable_shared_examples.rb @@ -36,3 +36,76 @@ shared_examples 'system notes for milestones' do end end end + +shared_examples 'updating a single task' do + def update_issuable(opts) + issuable = try(:issue) || try(:merge_request) + described_class.new(project, user, opts).execute(issuable) + end + + before do + update_issuable(description: "- [ ] Task 1\n- [ ] Task 2") + end + + context 'when a task is marked as completed' do + before do + update_issuable(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 }) + end + + it 'creates system note about task status change' do + note1 = find_note('marked the task **Task 1** as completed') + + expect(note1).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) + end + end + + context 'when a task is marked as incomplete' do + before do + update_issuable(description: "- [x] Task 1\n- [X] Task 2") + update_issuable(update_task: { index: 2, checked: false, line_source: '- [X] Task 2', line_number: 2 }) + end + + it 'creates system note about task status change' do + note1 = find_note('marked the task **Task 2** as incomplete') + + expect(note1).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) + end + end + + context 'when the task position has been modified' do + before do + update_issuable(description: "- [ ] Task 1\n- [ ] Task 3\n- [ ] Task 2") + end + + it 'raises an exception' do + expect(Note.count).to eq(2) + expect do + update_issuable(update_task: { index: 2, checked: true, line_source: '- [ ] Task 2', line_number: 2 }) + end.to raise_error(ActiveRecord::StaleObjectError) + expect(Note.count).to eq(2) + end + end + + context 'when the content changes but not task line number' do + before do + update_issuable(description: "Paragraph\n\n- [ ] Task 1\n- [x] Task 2") + update_issuable(description: "Paragraph with more words\n\n- [ ] Task 1\n- [x] Task 2") + update_issuable(update_task: { index: 2, checked: false, line_source: '- [x] Task 2', line_number: 4 }) + end + + it 'creates system note about task status change' do + note1 = find_note('marked the task **Task 2** as incomplete') + + expect(note1).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(2) + end + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 554f2e747bc..0b19b50fdfc 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -48,6 +48,36 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.version).to eq(subject.class.const_get(:VERSION)) end + + context 'application is updating' do + subject { create(application_name, :updating) } + + it 'is updated' do + subject.make_installed! + + expect(subject).to be_updated + end + + it 'updates helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') + + subject.make_installed! + + subject.cluster.application_helm.reload + + expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) + end + + it 'updates the version of the application' do + subject.update!(version: '0.0.0') + + subject.make_installed! + + subject.reload + + expect(subject.version).to eq(subject.class.const_get(:VERSION)) + end + end end describe '#make_updated' do @@ -90,6 +120,17 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_errored expect(subject.status_reason).to eq(reason) end + + context 'application is updating' do + subject { create(application_name, :updating) } + + it 'is update_errored' do + subject.make_errored(reason) + + expect(subject).to be_update_errored + expect(subject.status_reason).to eq(reason) + end + end end describe '#make_scheduled' do @@ -112,6 +153,18 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.status_reason).to be_nil end end + + describe 'when was updated_errored' do + subject { create(application_name, :update_errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end end end diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb new file mode 100644 index 00000000000..181b102e685 --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +shared_examples 'cluster application version specs' do |application_name| + describe 'update_available?' do + let(:version) { '0.0.0' } + + subject { create(application_name, :installed, version: version).update_available? } + + context 'version is not the same as VERSION' do + it { is_expected.to be_truthy } + end + + context 'version is the same as VERSION' do + let(:application) { build(application_name) } + let(:version) { application.class.const_get(:VERSION) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb index cb1b9e6f5fb..2a2539c80b5 100644 --- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb @@ -7,56 +7,9 @@ describe 'projects/settings/ci_cd/_autodevops_form' do assign :project, project end - context 'when kubernetes is not active' do - context 'when auto devops domain is not defined' do - it 'shows warning message' do - render + it 'shows a warning message about Kubernetes cluster' do + render - expect(rendered).to have_css('.auto-devops-warning-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a') - expect(rendered).to have_link('Kubernetes cluster') - end - end - - context 'when auto devops domain is defined' do - before do - project.build_auto_devops(domain: 'example.com') - end - - it 'shows warning message' do - render - - expect(rendered).to have_css('.auto-devops-warning-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a') - expect(rendered).to have_link('Kubernetes cluster') - end - end - end - - context 'when kubernetes is active' do - before do - create(:kubernetes_service, project: project) - end - - context 'when auto devops domain is not defined' do - it 'shows warning message' do - render - - expect(rendered).to have_css('.auto-devops-warning-message') - expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.') - end - end - - context 'when auto devops domain is defined' do - before do - project.build_auto_devops(domain: 'example.com') - end - - it 'does not show warning message' do - render - - expect(rendered).not_to have_css('.auto-devops-warning-message') - end - end + expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.') end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 9176eb12b12..caae46a3175 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -141,11 +141,18 @@ describe PostReceive do let(:gl_repository) { "wiki-#{project.id}" } it 'updates project activity' do - described_class.new.perform(gl_repository, key_id, base64_changes) + # Force Project#set_timestamps_for_create to initialize timestamps + project - expect { project.reload } - .to change(project, :last_activity_at) - .and change(project, :last_repository_updated_at) + # MySQL drops milliseconds in the timestamps, so advance at least + # a second to ensure we see changes. + Timecop.freeze(1.second.from_now) do + expect do + described_class.new.perform(gl_repository, key_id, base64_changes) + project.reload + end.to change(project, :last_activity_at) + .and change(project, :last_repository_updated_at) + end end end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 781f91ac9ca..31bfe88d0bd 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -24,12 +24,7 @@ describe RepositoryForkWorker do end def expect_fork_repository - expect(shell).to receive(:fork_repository).with( - 'default', - project.disk_path, - forked_project.repository_storage, - forked_project.disk_path - ) + expect(shell).to receive(:fork_repository).with(project, forked_project) end describe 'when a worker was reset without cleanup' do diff --git a/vendor/project_templates/gitbook.tar.gz b/vendor/project_templates/gitbook.tar.gz Binary files differnew file mode 100644 index 00000000000..73062fca038 --- /dev/null +++ b/vendor/project_templates/gitbook.tar.gz diff --git a/vendor/project_templates/hexo.tar.gz b/vendor/project_templates/hexo.tar.gz Binary files differnew file mode 100644 index 00000000000..b32c4945366 --- /dev/null +++ b/vendor/project_templates/hexo.tar.gz diff --git a/vendor/project_templates/hugo.tar.gz b/vendor/project_templates/hugo.tar.gz Binary files differnew file mode 100644 index 00000000000..4bdb03f5b2f --- /dev/null +++ b/vendor/project_templates/hugo.tar.gz diff --git a/vendor/project_templates/jekyll.tar.gz b/vendor/project_templates/jekyll.tar.gz Binary files differnew file mode 100644 index 00000000000..ab61ddd03ea --- /dev/null +++ b/vendor/project_templates/jekyll.tar.gz diff --git a/vendor/project_templates/plainhtml.tar.gz b/vendor/project_templates/plainhtml.tar.gz Binary files differnew file mode 100644 index 00000000000..6927ae74de8 --- /dev/null +++ b/vendor/project_templates/plainhtml.tar.gz diff --git a/yarn.lock b/yarn.lock index 423c7f75d47..9d0ba6640f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -653,10 +653,10 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.48.0": - version "1.48.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.48.0.tgz#7b2e20e357d85aa46e905e6ca51b0b4184ae2794" - integrity sha512-9lRsfqN0W3JxopiXnTzvDY31O465jMTGNKpiOCXy7uAMfwZA6UsRsc7Pp369uKnOLR0duXUGOxOv4NGsK6AeXw== +"@gitlab/svgs@^1.51.0": + version "1.51.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.51.0.tgz#1b608f68dfb74284401b1cbdb823440f6e8b0091" + integrity sha512-B1Wdhfy5ZClkHuaaCUUZyOBF8CFxxHqxGGhveRekOowtlMExa3tx+YkqNa5XPsEVMF6Aqnh8evQmmN4b+zrHVQ== "@gitlab/ui@^2.0.2": version "2.0.2" |