diff options
333 files changed, 5656 insertions, 1726 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b93a79de994..1ea9ce1f497 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -388,7 +388,7 @@ 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:v5" paths: - vendor/ruby/ - .yarn-cache/ @@ -636,7 +636,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 744068368fb..815d5ca06d5 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.18.0
\ No newline at end of file +1.19.0 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..7e00b994541 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -13,18 +13,43 @@ export default { Icon, FileRow, }, + data() { + return { + search: '', + }; + }, 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; + const search = this.search.toLowerCase().trim(); + + if (search === '' || this.$options.fuzzyFileFinderEnabled) + return this.renderTreeList ? this.tree : this.allBlobs; + + return this.allBlobs.reduce((acc, folder) => { + const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); + + if (tree.length) { + return acc.concat({ + ...folder, + tree, + }); + } + + return acc; + }, []); }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), + clearSearch() { + this.search = ''; + }, }, shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, FileRowStats, + diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, }; </script> @@ -33,17 +58,36 @@ export default { <div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="flex-fill d-flex"> <icon name="search" class="position-absolute tree-list-icon" /> - <button - type="button" - class="form-control text-left text-secondary" - @click="toggleFileFinder(true)" - > - {{ s__('MergeRequest|Search files') }} - </button> - <span - class="position-absolute text-secondary diff-tree-search-shortcut" - v-html="$options.shortcutKeyCharacter" - ></span> + <template v-if="$options.diffTreeFiltering"> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + /> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon name="close" /> + </button> + </template> + <template v-else> + <button + type="button" + class="form-control text-left text-secondary" + @click="toggleFileFinder(true)" + > + {{ s__('MergeRequest|Search files') }} + </button> + <span + class="position-absolute text-secondary diff-tree-search-shortcut" + v-html="$options.shortcutKeyCharacter" + ></span> + </template> </div> </div> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> @@ -64,13 +108,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> @@ -86,7 +123,7 @@ export default { pointer-events: none; } -.tree-list-icon { +.tree-list-icon:not(button) { pointer-events: none; } </style> 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/ide/index.js b/app/assets/javascripts/ide/index.js index 5a2b680c2f7..cdfebd19fa4 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -6,6 +6,7 @@ import ide from './components/ide.vue'; import store from './stores'; import router from './ide_router'; import { parseBoolean } from '../lib/utils/common_utils'; +import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; Vue.use(Translate); @@ -60,16 +61,6 @@ export function initIde(el, options = {}) { }); } -// tell webpack to load assets from origin so that web workers don't break -export function resetServiceWorkersPublicPath() { - // __webpack_public_path__ is a global variable that can be used to adjust - // the webpack publicPath setting at runtime. - // see: https://webpack.js.org/guides/public-path/ - const relativeRootPath = (gon && gon.relative_url_root) || ''; - const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; - __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase -} - /** * Start the IDE. * 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/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js new file mode 100644 index 00000000000..0f78756aac8 --- /dev/null +++ b/app/assets/javascripts/lib/utils/chart_utils.js @@ -0,0 +1,83 @@ +const commonTooltips = () => ({ + mode: 'x', + intersect: false, +}); + +const adjustedFontScale = () => ({ + fontSize: 8, +}); + +const yAxesConfig = (shouldAdjustFontSize = false) => ({ + yAxes: [ + { + ticks: { + beginAtZero: true, + ...(shouldAdjustFontSize ? adjustedFontScale() : {}), + }, + }, + ], +}); + +const xAxesConfig = (shouldAdjustFontSize = false) => ({ + xAxes: [ + { + ticks: { + ...(shouldAdjustFontSize ? adjustedFontScale() : {}), + }, + }, + ], +}); + +const commonChartOptions = () => ({ + responsive: true, + maintainAspectRatio: false, + legend: false, +}); + +export const barChartOptions = shouldAdjustFontSize => ({ + ...commonChartOptions(), + scales: { + ...yAxesConfig(shouldAdjustFontSize), + ...xAxesConfig(shouldAdjustFontSize), + }, + tooltips: { + ...commonTooltips(), + displayColors: false, + callbacks: { + title() { + return ''; + }, + label({ xLabel, yLabel }) { + return `${xLabel}: ${yLabel}`; + }, + }, + }, +}); + +export const pieChartOptions = commonChartOptions; + +export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize }) => ({ + ...commonChartOptions(), + scales: { + ...yAxesConfig(shouldAdjustFontSize), + ...xAxesConfig(shouldAdjustFontSize), + }, + elements: { + point: { + hitRadius: width / (numberOfPoints * 2), + }, + }, + tooltips: { + ...commonTooltips(), + caretSize: 0, + multiKeyBackground: 'rgba(0,0,0,0)', + callbacks: { + labelColor({ datasetIndex }, { config }) { + return { + backgroundColor: config.data.datasets[datasetIndex].backgroundColor, + borderColor: 'rgba(0,0,0,0)', + }; + }, + }, + }, +}); 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/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js new file mode 100644 index 00000000000..308ad9784e4 --- /dev/null +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -0,0 +1,10 @@ +// tell webpack to load assets from origin so that web workers don't break +// eslint-disable-next-line import/prefer-default-export +export function resetServiceWorkersPublicPath() { + // __webpack_public_path__ is a global variable that can be used to adjust + // the webpack publicPath setting at runtime. + // see: https://webpack.js.org/guides/public-path/ + const relativeRootPath = (gon && gon.relative_url_root) || ''; + const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; + __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase +} 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/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index e4d72eb8318..9e99aa4f724 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -7,8 +7,11 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; import initDiscussionFilters from '../notes/discussion_filters'; import store from './stores'; import MergeRequest from '../merge_request'; +import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; export default function initMrNotes() { + resetServiceWorkersPublicPath(); + const mrShowNode = document.querySelector('.merge-request'); // eslint-disable-next-line no-new new MergeRequest({ diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 394f2a80a67..91b9e5de374 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -135,6 +135,7 @@ export default { <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span> <div v-if="canResolve" class="note-actions-item"> <button + ref="resolveButton" v-gl-tooltip :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" :title="resolveButtonTitle" @@ -151,10 +152,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 +175,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 +186,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 +197,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..314519ee442 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -2,65 +2,86 @@ import $ from 'jquery'; import Chart from 'chart.js'; import _ from 'underscore'; +import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils'; + document.addEventListener('DOMContentLoaded', () => { const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); - const responsiveChart = (selector, data) => { - const options = { - scaleOverlay: true, - responsive: true, - pointHitDetectionRadius: 2, - maintainAspectRatio: false, - }; + const barChart = (selector, data) => { // get selector by context const ctx = selector.get(0).getContext('2d'); // pointing parent container to make chart.js inherit its width const container = $(selector).parent(); - const generateChart = () => { - selector.attr('width', $(container).width()); - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - return new Chart(ctx).Bar(data, options); - }; - // enabling auto-resizing - $(window).resize(generateChart); - return generateChart(); + selector.attr('width', $(container).width()); + + // Scale fonts if window width lower than 768px (iPad portrait) + const shouldAdjustFontSize = window.innerWidth < 768; + return new Chart(ctx, { + type: 'bar', + data, + options: barChartOptions(shouldAdjustFontSize), + }); + }; + + const pieChart = (context, data) => { + const options = pieChartOptions(); + + return new Chart(context, { + type: 'pie', + data, + options, + }); }; const chartData = data => ({ labels: Object.keys(data), datasets: [ { - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, - barValueSpacing: 1, - barDatasetSpacing: 1, + backgroundColor: 'rgba(220,220,220,0.5)', + borderColor: 'rgba(220,220,220,1)', + borderWidth: 1, data: _.values(data), }, ], }); + 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); + barChart($('#hour-chart'), hourData); - const dayData = chartData(projectChartData.weekDays); - responsiveChart($('#weekday-chart'), dayData); + const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); + const dayData = chartData(weekDays); + barChart($('#weekday-chart'), dayData); const monthData = chartData(projectChartData.month); - responsiveChart($('#month-chart'), monthData); + barChart($('#month-chart'), monthData); - const data = projectChartData.languages; + const data = { + datasets: [ + { + data: projectChartData.languages.map(x => x.value), + backgroundColor: projectChartData.languages.map(x => x.color), + hoverBackgroundColor: projectChartData.languages.map(x => x.highlight), + }, + ], + labels: projectChartData.languages.map(x => x.label), + }; const ctx = $('#languages-chart') .get(0) .getContext('2d'); - const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, - }; - - new Chart(ctx).Pie(data, options); + pieChart(ctx, data); }); diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index 48353f3b4ef..9fa580d2ba9 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -1,29 +1,31 @@ import $ from 'jquery'; import Chart from 'chart.js'; -const options = { - scaleOverlay: true, - responsive: true, - maintainAspectRatio: false, -}; +import { barChartOptions, lineChartOptions } from '~/lib/utils/chart_utils'; + +const SUCCESS_LINE_COLOR = '#1aaa55'; + +const TOTAL_LINE_COLOR = '#707070'; -const buildChart = chartScope => { +const buildChart = (chartScope, shouldAdjustFontSize) => { const data = { labels: chartScope.labels, datasets: [ { - fillColor: '#707070', - strokeColor: '#707070', - pointColor: '#707070', - pointStrokeColor: '#EEE', - data: chartScope.totalValues, + backgroundColor: SUCCESS_LINE_COLOR, + borderColor: SUCCESS_LINE_COLOR, + pointBackgroundColor: SUCCESS_LINE_COLOR, + pointBorderColor: '#fff', + data: chartScope.successValues, + fill: 'origin', }, { - fillColor: '#1aaa55', - strokeColor: '#1aaa55', - pointColor: '#1aaa55', - pointStrokeColor: '#fff', - data: chartScope.successValues, + backgroundColor: TOTAL_LINE_COLOR, + borderColor: TOTAL_LINE_COLOR, + pointBackgroundColor: TOTAL_LINE_COLOR, + pointBorderColor: '#EEE', + data: chartScope.totalValues, + fill: '-1', }, ], }; @@ -31,36 +33,51 @@ const buildChart = chartScope => { .get(0) .getContext('2d'); - new Chart(ctx).Line(data, options); + return new Chart(ctx, { + type: 'line', + data, + options: lineChartOptions({ + width: ctx.canvas.width, + numberOfPoints: chartScope.totalValues.length, + shouldAdjustFontSize, + }), + }); }; -document.addEventListener('DOMContentLoaded', () => { - const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); - const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); +const buildBarChart = (chartTimesData, shouldAdjustFontSize) => { const data = { labels: chartTimesData.labels, datasets: [ { - fillColor: 'rgba(220,220,220,0.5)', - strokeColor: 'rgba(220,220,220,1)', - barStrokeWidth: 1, + backgroundColor: 'rgba(220,220,220,0.5)', + borderColor: 'rgba(220,220,220,1)', + borderWidth: 1, barValueSpacing: 1, barDatasetSpacing: 1, data: chartTimesData.values, }, ], }; - - if (window.innerWidth < 768) { - // Scale fonts if window width lower than 768px (iPad portrait) - options.scaleFontSize = 8; - } - - new Chart( + return new Chart( $('#build_timesChart') .get(0) .getContext('2d'), - ).Bar(data, options); + { + type: 'bar', + data, + options: barChartOptions(shouldAdjustFontSize), + }, + ); +}; + +document.addEventListener('DOMContentLoaded', () => { + const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); + const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); + + // Scale fonts if window width lower than 768px (iPad portrait) + const shouldAdjustFontSize = window.innerWidth < 768; + + buildBarChart(chartTimesData, shouldAdjustFontSize); - chartsData.forEach(scope => buildChart(scope)); + chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize)); }); 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/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 74faa35358d..1ec2784cc5a 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -134,6 +134,13 @@ export default { >ms / <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span> gc </span> </div> + <div + v-if="currentRequest.details && currentRequest.details.tracing" + id="peek-view-trace" + class="view" + > + <a :href="currentRequest.details.tracing.tracing_url"> trace </a> + </div> <request-selector v-if="currentRequest" :current-request="currentRequest" 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..fa502b9beb9 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,6 +1,7 @@ <script> import Pikaday from 'pikaday'; import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; +import { __ } from '~/locale'; export default { name: 'DatePicker', @@ -8,7 +9,7 @@ export default { label: { type: String, required: false, - default: 'Date picker', + default: __('Date picker'), }, selectedDate: { type: Date, @@ -40,6 +41,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/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 6c0c7f15943..45f01a6fced 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -4,6 +4,7 @@ import datePicker from '../pikaday.vue'; import toggleSidebar from './toggle_sidebar.vue'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; import { dateInWords } from '../../../lib/utils/datetime_utility'; +import { __ } from '~/locale'; export default { name: 'SidebarDatePicker', @@ -42,7 +43,7 @@ export default { label: { type: String, required: false, - default: 'Date picker', + default: __('Date picker'), }, selectedDate: { type: Date, 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 4d8f1ee51a6..6108eaa1ad0 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -52,6 +52,7 @@ position: absolute; top: 5px; right: 15px; + margin-left: auto; .btn { padding: 0 10px; @@ -325,6 +326,7 @@ span.idiff { &, .file-holder & { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; background-color: $gray-light; @@ -367,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 7088a6f59dc..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; 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 a225645c643..e3b98b26a11 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -508,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/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/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5cf7fa3422d..46a44841c31 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -16,6 +16,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] + before_action only: [:show] do + push_frontend_feature_flag(:diff_tree_filtering, default_enabled: true) + end + def index @merge_requests = @issuables 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..5c0164831bc 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,18 +29,16 @@ 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 - end - - event :make_updated do - transition [:updating] => :updated + transition [:installed, :updated, :update_errored, :scheduled] => :updating end event :make_update_errored do @@ -74,6 +72,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..46d0898014e 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -43,6 +43,7 @@ module Clusters # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) validates :api_url, url: true, presence: true validates :token, presence: true + validates :ca_cert, certificate: true, allow_blank: true, if: :ca_cert_changed? validate :prevent_modification, on: :update @@ -98,6 +99,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/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/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/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/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/blob/viewers/_loading.html.haml b/app/views/projects/blob/viewers/_loading.html.haml index 120c0540335..df1f3e4e01b 100644 --- a/app/views/projects/blob/viewers/_loading.html.haml +++ b/app/views/projects/blob/viewers/_loading.html.haml @@ -1,2 +1,2 @@ .text-center.prepend-top-default.append-bottom-default - = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…') + = icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…', class: 'qa-spinner') 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/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index b0e22a35fe1..60160f521ad 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -55,23 +55,23 @@ #{@commits_graph.authors} = (_("Authors: %{authors}") % { authors: "<strong>#{authors}</strong>" }).html_safe .col-md-6 + %p.slead + = _("Commits per day of month") %div - %p.slead - = _("Commits per day of month") %canvas#month-chart .row .col-md-6 .col-md-6 + %p.slead + = _("Commits per weekday") %div - %p.slead - = _("Commits per weekday") %canvas#weekday-chart .row .col-md-6 .col-md-6 + %p.slead + = _("Commits per day hour (UTC)") %div - %p.slead - = _("Commits per day hour (UTC)") %canvas#hour-chart -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml index 43e4c8db93f..90838a75214 100644 --- a/app/views/projects/issues/_merge_requests_status.html.haml +++ b/app/views/projects/issues/_merge_requests_status.html.haml @@ -12,11 +12,14 @@ - mr_status_class = 'closed' - else - mr_status_date = merge_request.created_at - - mr_status_title = _('Opened') + - mr_status_title = mr_status_date ? _('Opened') : _('Open') - mr_status_icon = 'issue-open-m' - mr_status_class = 'open' -- mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" +- if mr_status_date + - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" +- else + - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>" %span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } } = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}") 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/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml index c23fe6ff170..c0ac79ed5f8 100644 --- a/app/views/projects/pipelines/charts/_pipeline_times.haml +++ b/app/views/projects/pipelines/charts/_pipeline_times.haml @@ -1,7 +1,7 @@ -%div - %p.light - = _("Commit duration in minutes for last 30 commits") +%p.light + = _("Commit duration in minutes for last 30 commits") +%div %canvas#build_timesChart{ height: 200 } -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index 14b3d47a9c2..47f1f074210 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -13,18 +13,21 @@ %p.light = _("Pipelines for last week") (#{date_from_to(Date.today - 7.days, Date.today)}) - %canvas#weekChart{ height: 200 } + %div + %canvas#weekChart{ height: 200 } .prepend-top-default %p.light = _("Pipelines for last month") (#{date_from_to(Date.today - 30.days, Date.today)}) - %canvas#monthChart{ height: 200 } + %div + %canvas#monthChart{ height: 200 } .prepend-top-default %p.light = _("Pipelines for last year") - %canvas#yearChart.padded{ height: 250 } + %div + %canvas#yearChart.padded{ height: 250 } -# haml-lint:disable InlineJavaScript %script#pipelinesChartsData{ type: "application/json" } 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/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/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/55447-validate-k8s-ca-cert.yml b/changelogs/unreleased/55447-validate-k8s-ca-cert.yml new file mode 100644 index 00000000000..e0448d403da --- /dev/null +++ b/changelogs/unreleased/55447-validate-k8s-ca-cert.yml @@ -0,0 +1,5 @@ +--- +title: Validate kubernetes cluster CA certificate +merge_request: 24990 +author: +type: changed 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/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/an-peek-jaeger.yml b/changelogs/unreleased/an-peek-jaeger.yml new file mode 100644 index 00000000000..8659ee4f9e0 --- /dev/null +++ b/changelogs/unreleased/an-peek-jaeger.yml @@ -0,0 +1,5 @@ +--- +title: Provide a performance bar link to the Jaeger UI +merge_request: 24902 +author: +type: other 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/deprecated-migration-inheritance.yml b/changelogs/unreleased/deprecated-migration-inheritance.yml new file mode 100644 index 00000000000..814c511195b --- /dev/null +++ b/changelogs/unreleased/deprecated-migration-inheritance.yml @@ -0,0 +1,5 @@ +--- +title: Directly inheriting from ActiveRecord::Migration is deprecated +merge_request: 25066 +author: Jasper Maes +type: other 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/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/peek.rb b/config/initializers/peek.rb index a6f43415ec5..e051e5c68c4 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -19,6 +19,7 @@ Peek.into Peek::Views::Gitaly Peek.into Peek::Views::Rblineprof Peek.into Peek::Views::Redis Peek.into Peek::Views::GC +Peek.into Peek::Views::Tracing if Gitlab::Tracing.tracing_url_enabled? # rubocop:disable Naming/ClassAndModuleCamelCase class PEEK_DB_CLIENT diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index 462e8c811a6..151cad3ef9a 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -173,7 +173,7 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d def connect(*args) val = super - if current_transaction = Gitlab::Metrics::Transaction.current + if current_transaction = ::Gitlab::Metrics::Transaction.current current_transaction.increment(:new_redis_connections, 1) end 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..96ba5fbd816 --- /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[5.0] + 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..e9a8c1011ad --- /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[5.0] + 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/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/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md index b561c2f82aa..7d245b3effd 100644 --- a/doc/administration/restart_gitlab.md +++ b/doc/administration/restart_gitlab.md @@ -62,7 +62,8 @@ sudo gitlab-ctl status Notice that all services say `ok: run`. -Sometimes, components time out during the restart and sometimes they get stuck. +Sometimes, components time out (look for `timeout` in the logs) during the +restart and sometimes they get stuck. In that case, you can use `gitlab-ctl kill <service>` to send the `SIGKILL` signal to the service, for example `sidekiq`. After that, a restart should perform fine. diff --git a/doc/api/README.md b/doc/api/README.md index 692f63a400c..3b43d195390 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) @@ -53,6 +54,7 @@ The following API resources are available: - [Project clusters](project_clusters.md) - [Project-level variables](project_level_variables.md) - [Project import/export](project_import_export.md) + - [Project import from GitHub](import.md) - [Project members](members.md) - [Project milestones](milestones.md) - [Project snippets](project_snippets.md) diff --git a/doc/api/branches.md b/doc/api/branches.md index 8d5f333ba77..62468b6e917 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -18,11 +18,10 @@ GET /projects/:id/repository/branches Parameters: -| Attribute | Type | Required | Description | -|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| -| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | -| `search` | string | no | Return list of branches matching the search criteria. | - +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user.| +| `search` | string | no | Return list of branches containing the search string. You can use `^term` and `term$` to find branches that begin and end with `term` respectively.| Example request: ```sh diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index fe370682308..357d9916ade 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -1,18 +1,25 @@ # Broadcast Messages API -> **Note:** This feature was introduced in GitLab 8.12. +> Introduced in GitLab 8.12. -The broadcast message API is only accessible to administrators. All requests by -guests will respond with `401 Unauthorized`, and all requests by normal users -will respond with `403 Forbidden`. +Broadcast messages API operates on [broadcast messages](../user/admin_area/broadcast_messages.md). + +The broadcast message API is only accessible to administrators. All requests by: + +- Guests will result in `401 Unauthorized`. +- Regular users will result in `403 Forbidden`. ## Get all broadcast messages -``` +List all broadcast messages. + +```text GET /broadcast_messages ``` -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/broadcast_messages ``` @@ -34,15 +41,21 @@ Example response: ## Get a specific broadcast message -``` +Get a specific broadcast message. + +```text GET /broadcast_messages/:id ``` -| Attribute | Type | Required | Description | -| ----------- | -------- | -------- | ------------------------- | -| `id` | integer | yes | Broadcast message ID | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-------------------------------------| +| `id` | integer | yes | ID of broadcast message to retrieve. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` @@ -62,19 +75,25 @@ Example response: ## Create a broadcast message -``` +Create a new broadcast message. + +```text POST /broadcast_messages ``` -| Attribute | Type | Required | Description | -| ----------- | -------- | -------- | ---------------------------------------------------- | -| `message` | string | yes | Message to display | -| `starts_at` | datetime | no | Starting time (defaults to current time) | -| `ends_at` | datetime | no | Ending time (defaults to one hour from current time) | -| `color` | string | no | Background color hex code | -| `font` | string | no | Foreground color hex code | +Parameters: -```bash +| Attribute | Type | Required | Description | +|:------------|:---------|:---------|:------------------------------------------------------| +| `message` | string | yes | Message to display. | +| `starts_at` | datetime | no | Starting time (defaults to current time). | +| `ends_at` | datetime | no | Ending time (defaults to one hour from current time). | +| `color` | string | no | Background color hex code. | +| `font` | string | no | Foreground color hex code. | + +Example request: + +```sh curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/broadcast_messages ``` @@ -94,20 +113,26 @@ Example response: ## Update a broadcast message -``` +Update an existing broadcast message. + +```text PUT /broadcast_messages/:id ``` -| Attribute | Type | Required | Description | -| ----------- | -------- | -------- | ------------------------- | -| `id` | integer | yes | Broadcast message ID | -| `message` | string | no | Message to display | -| `starts_at` | datetime | no | Starting time | -| `ends_at` | datetime | no | Ending time | -| `color` | string | no | Background color hex code | -| `font` | string | no | Foreground color hex code | +Parameters: + +| Attribute | Type | Required | Description | +|:------------|:---------|:---------|:-----------------------------------| +| `id` | integer | yes | ID of broadcast message to update. | +| `message` | string | no | Message to display. | +| `starts_at` | datetime | no | Starting time. | +| `ends_at` | datetime | no | Ending time. | +| `color` | string | no | Background color hex code. | +| `font` | string | no | Foreground color hex code. | -```bash +Example request: + +```sh curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` @@ -127,14 +152,20 @@ Example response: ## Delete a broadcast message -``` +Delete a broadcast message. + +```sh DELETE /broadcast_messages/:id ``` -| Attribute | Type | Required | Description | -| ----------- | -------- | -------- | ------------------------- | -| `id` | integer | yes | Broadcast message ID | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-----------------------------------| +| `id` | integer | yes | ID of broadcast message to delete. | + +Example request: -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/broadcast_messages/1 ``` diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md index b70854103e8..c77ed39e4dc 100644 --- a/doc/api/container_registry.md +++ b/doc/api/container_registry.md @@ -1,5 +1,7 @@ # Container Registry API +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/55978) in GitLab 11.8. + This is the API docs of the [GitLab Container Registry](../user/project/container_registry.md). ## List registry repositories @@ -42,7 +44,7 @@ Example response: ## Delete registry repository -Get a list of repository commits in a project. +Delete a repository in registry. This operation is executed asynchronously and might take some time to get executed. 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/api/snippets.md b/doc/api/snippets.md index 2cbd041d132..f90447e124e 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -1,43 +1,100 @@ # Snippets API -> [Introduced][ce-6373] in GitLab 8.15. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6373) in GitLab 8.15. + +Snippets API operates on [snippets](../user/snippets.md). ## Snippet visibility level Snippets in GitLab can be either private, internal, or public. You can set it with the `visibility` field in the snippet. -Constants for snippet visibility levels are: +Valid values for snippet visibility levels are: -| Visibility | Description | -| ---------- | ----------- | -| `private` | The snippet is visible only to the snippet creator | -| `internal` | The snippet is visible for any logged in user | -| `public` | The snippet can be accessed without any authentication | +| Visibility | Description | +|:-----------|:----------------------------------------------------| +| `private` | Snippet is visible only to the snippet creator. | +| `internal` | Snippet is visible for any logged in user. | +| `public` | Snippet can be accessed without any authentication. | -## List snippets +## List all snippets for a user -Get a list of current user's snippets. +Get a list of the current user's snippets. -``` +```text GET /snippets ``` -## Single snippet +Example request: -Get a single snippet. +```sh +curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/snippets +``` + +Example response: +```json +[ + { + "id": 42, + "title": "Voluptatem iure ut qui aut et consequatur quaerat.", + "file_name": "mclaughlin.rb", + "description": null, + "visibility": "internal", + "author": { + "id": 22, + "name": "User 0", + "username": "user0", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon", + "web_url": "http://localhost:3000/user0" + }, + "updated_at": "2018-09-18T01:12:26.383Z", + "created_at": "2018-09-18T01:12:26.383Z", + "project_id": null, + "web_url": "http://localhost:3000/snippets/42", + "raw_url": "http://localhost:3000/snippets/42/raw" + }, + { + "id": 41, + "title": "Ut praesentium non et atque.", + "file_name": "ondrickaemard.rb", + "description": null, + "visibility": "internal", + "author": { + "id": 22, + "name": "User 0", + "username": "user0", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon", + "web_url": "http://localhost:3000/user0" + }, + "updated_at": "2018-09-18T01:12:26.360Z", + "created_at": "2018-09-18T01:12:26.360Z", + "project_id": null, + "web_url": "http://localhost:3000/snippets/41", + "raw_url": "http://localhost:3000/snippets/41/raw" + } +] ``` + +## Get a single snippet + +Get a single snippet. + +```text GET /snippets/:id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | Integer | yes | The ID of a snippet | +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:---------------------------| +| `id` | integer | yes | ID of snippet to retrieve. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/snippets/1 ``` @@ -69,46 +126,52 @@ Example response: Get a single snippet's raw contents. -``` +```text GET /snippets/:id/raw ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | Integer | yes | The ID of a snippet | +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:---------------------------| +| `id` | integer | yes | ID of snippet to retrieve. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/snippets/1/raw ``` Example response: -``` +```text Hello World snippet ``` ## Create new snippet -Creates a new snippet. The user must have permission to create new snippets. +Create a new snippet. -``` +NOTE: **Note:** +The user must have permission to create new snippets. + +```text POST /snippets ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `title` | String | yes | The title of a snippet | -| `file_name` | String | yes | The name of a snippet file | -| `content` | String | yes | The content of a snippet | -| `description` | String | no | The description of a snippet | -| `visibility` | String | no | The snippet's visibility | +| Attribute | Type | Required | Description | +|:--------------|:-------|:---------|:---------------------------------------------------| +| `title` | string | yes | Title of a snippet. | +| `file_name` | string | yes | Name of a snippet file. | +| `content` | string | yes | Content of a snippet. | +| `description` | string | no | Description of a snippet. | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | +Example request: -```bash +```sh curl --request POST \ --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \ --header 'Content-Type: application/json' \ @@ -142,25 +205,29 @@ Example response: ## Update snippet -Updates an existing snippet. The user must have permission to change an existing snippet. +Update an existing snippet. -``` +NOTE: **Note:** +The user must have permission to change an existing snippet. + +```text PUT /snippets/:id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | Integer | yes | The ID of a snippet | -| `title` | String | no | The title of a snippet | -| `file_name` | String | no | The name of a snippet file | -| `description` | String | no | The description of a snippet | -| `content` | String | no | The content of a snippet | -| `visibility` | String | no | The snippet's visibility | +| Attribute | Type | Required | Description | +|:--------------|:--------|:---------|:---------------------------------------------------| +| `id` | integer | yes | ID of snippet to update. | +| `title` | string | no | Title of a snippet. | +| `file_name` | string | no | Name of a snippet file. | +| `description` | string | no | Description of a snippet. | +| `content` | string | no | Content of a snippet. | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | +Example request: -```bash +```sh curl --request PUT \ --data '{"title": "foo", "content": "bar"}' \ --header 'Content-Type: application/json' \ @@ -194,38 +261,49 @@ Example response: ## Delete snippet -Deletes an existing snippet. +Delete an existing snippet. -``` +```text DELETE /snippets/:id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | Integer | yes | The ID of a snippet | +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-------------------------| +| `id` | integer | yes | ID of snippet to delete. | +Example request: -``` +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/snippets/1" ``` -upon successful delete a `204 No content` HTTP code shall be expected, with no data, -but if the snippet is non-existent, a `404 Not Found` will be returned. +The following are possible return codes: -## Explore all public snippets +| Code | Description | +|:------|:--------------------------------------------| +| `204` | Delete was successful. No data is returned. | +| `404` | The snippet wasn't found. | -``` +## List all public snippets + +List all public snippets. + +```text GET /snippets/public ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `per_page` | Integer | no | number of snippets to return per page | -| `page` | Integer | no | the page to retrieve | +Parameters: + +| Attribute | Type | Required | Description | +|:-----------|:--------|:---------|:---------------------------------------| +| `per_page` | integer | no | Number of snippets to return per page. | +| `page` | integer | no | Page to retrieve. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1 ``` @@ -273,21 +351,22 @@ Example response: ## Get user agent details -> **Notes:** -> [Introduced][ce-29508] in GitLab 9.4. - +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12655) in GitLab 9.4. -Available only for admins. +NOTE: **Note:** +Available only for administrators. -``` +```text GET /snippets/:id/user_agent_detail ``` -| Attribute | Type | Required | Description | -|-------------|---------|----------|--------------------------------------| -| `id` | Integer | yes | The ID of a snippet | +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:---------------| +| `id` | integer | yes | ID of snippet. | -```bash +Example request: + +```sh curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/snippets/1/user_agent_detail ``` @@ -300,6 +379,3 @@ Example response: "akismet_submitted": false } ``` - -[ce-6373]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6373 -[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12655 diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 8b3a7b63e62..f4d7b9ad194 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -60,7 +60,7 @@ To get a better understanding of Review Apps, review documentation on how enviro 1. Learn about [environments](../environments.md) and their role in the development workflow. 1. Learn about [CI variables](../variables/README.md) and how they can be used in your CI jobs. 1. Explore the [`environment` syntax](../yaml/README.md#environment) as defined in `.gitlab-ci.yml`. This will become a primary reference. -1. Additionally, find out about [manual actions](../environments.md#manual-actions) and how you can use them to deploy to critical environments like production with the push of a button. +1. Additionally, find out about [manual actions](../environments.md#manually-deploying-to-environments) and how you can use them to deploy to critical environments like production with the push of a button. 1. Follow the [example tutorials](#examples). These will guide you through setting up infrastructure and using Review Apps. ### Configuring dynamic environments diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 32c73c4f398..7498617ed2c 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -68,7 +68,7 @@ future GitLab releases.** | **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | -| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmenturl) is set. | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. | | **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. | | **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. | | **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index df14376dd36..984878b6c9b 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -776,7 +776,7 @@ In the above example we set up the `review_app` job to deploy to the `review` environment, and we also defined a new `stop_review_app` job under `on_stop`. Once the `review_app` job is successfully finished, it will trigger the `stop_review_app` job based on what is defined under `when`. In this case we -set it up to `manual` so it will need a [manual action](#manual-actions) via +set it up to `manual` so it will need a [manual action](#whenmanual) via GitLab's web interface in order to run. The `stop_review_app` job is **required** to have the following keywords defined: 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/documentation/index.md b/doc/development/documentation/index.md index 436d0a38f31..287a472d0d8 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -12,7 +12,7 @@ In addition to this page, the following resources to help craft and contribute d - [Structure and template](structure.md) - Learn the typical parts of a doc page and how to write each one. - [Workflow](workflow.md) - A landing page for our key workflows: - [Feature-change documentation workflow](feature-change-workflow.md) - Adding required documentation when developing a GitLab feature. - - [Documentation improvement worflow](improvement-workflow.md) - New content not associated with a new feature. + - [Documentation improvement workflow](improvement-workflow.md) - New content not associated with a new feature. - [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/) - A reference for the markdown implementation used by GitLab's documentation site and about.gitlab.com. - [Site architecture](site_architecture/index.md) - How docs.gitlab.com is built. @@ -78,7 +78,7 @@ you can immediately tell that you are navigating to user-related documentation about project features; specifically about merge requests. Our site's paths match those of our repository, so the clear structure also makes documentation easier to update. -While the documentation is home to a variety of content types, we do not organize by content type. +While the documentation is home to a variety of content types, we do not organize by content type. For example, do not create groupings of similar media types (e.g. indexes of all articles, videos, etc.). Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes it difficult to browse for the information you need and difficult to maintain up-to-date content. @@ -498,7 +498,7 @@ If you want to know the in-depth details, here's what's really happening: The following GitLab features are used among others: -- [Manual actions](../../ci/yaml/README.md#manual-actions) +- [Manual actions](../../ci/yaml/README.md#whenmanual) - [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) - [Review Apps](../../ci/review_apps/index.md) - [Artifacts](../../ci/yaml/README.md#artifacts) 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..91be3e3d45d 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). + 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,8 @@ 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). ## Enabling/Disabling Auto DevOps @@ -681,7 +684,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). 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 +714,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/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md index 51949088521..02445abdb37 100644 --- a/doc/user/admin_area/broadcast_messages.md +++ b/doc/user/admin_area/broadcast_messages.md @@ -4,6 +4,8 @@ GitLab can display messages to all users of a GitLab instance in a banner that a ![Broadcast Message](img/broadcast_messages.png) +Broadcast messages can be managed using the [broadcast messages API](../../api/broadcast_messages.md). + NOTE: **Note:** If more than one banner message is active at one time, they are displayed in a stack in order of creation. 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/permissions.md b/doc/user/permissions.md index 019652b2408..4976284aea4 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -180,7 +180,7 @@ group. | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit group | | | | | ✓ | | Create subgroup | | | | | ✓ | -| Create project in group | | | | ✓ | ✓ | +| Create project in group | | | ✓ | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | | Manage group labels | | ✓ | ✓ | ✓ | ✓ | 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 4de23efc51b..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 @@ -455,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/index.md b/doc/user/project/merge_requests/index.md index b4f5a72e148..593eb80e044 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -46,7 +46,7 @@ A. Consider you are a software developer working in a team: 1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD 1. You request the approval from your manager 1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter) -1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#manual-actions) for GitLab CI/CD +1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#whenmanual) for GitLab CI/CD 1. Your implementations were successfully shipped to your customer B. Consider you're a web developer writing a webpage for your company's: 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/user/snippets.md b/doc/user/snippets.md index 5c9f6ffb163..569bdc9e2d5 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -4,7 +4,12 @@ With GitLab Snippets you can store and share bits of code and text with other us ![GitLab Snippet](img/gitlab_snippet.png) -There are 2 types of snippets, personal snippets and project snippets. +Snippets can be maintained using [snippets API](../api/snippets.md). + +There are two types of snippets: + +- Personal snippets. +- Project snippets. ## Personal snippets 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/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/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/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/patch/sprockets_base_file_digest_key.rb b/lib/gitlab/patch/sprockets_base_file_digest_key.rb index 3925cdbbada..1c472638145 100644 --- a/lib/gitlab/patch/sprockets_base_file_digest_key.rb +++ b/lib/gitlab/patch/sprockets_base_file_digest_key.rb @@ -9,7 +9,7 @@ module Gitlab def file_digest(path) if stat = self.stat(path) digest = self.stat_digest(path, stat) - integrity_uri = self.hexdigest_integrity_uri(digest) + integrity_uri = self.integrity_uri(digest) key = Sprockets::UnloadedAsset.new(path, self).file_digest_key(integrity_uri) cache.fetch(key) do 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/tracing.rb b/lib/gitlab/tracing.rb index 3c4db42ac06..0d9b0be1c8e 100644 --- a/lib/gitlab/tracing.rb +++ b/lib/gitlab/tracing.rb @@ -13,5 +13,24 @@ module Gitlab def self.connection_string ENV['GITLAB_TRACING'] end + + def self.tracing_url_template + ENV['GITLAB_TRACING_URL'] + end + + def self.tracing_url_enabled? + enabled? && tracing_url_template.present? + end + + # This will provide a link into the distributed tracing for the current trace, + # if it has been captured. + def self.tracing_url + return unless tracing_url_enabled? + + tracing_url_template % { + correlation_id: Gitlab::CorrelationId.current_id.to_s, + service: Gitlab.process_name + } + end end end diff --git a/lib/peek/views/tracing.rb b/lib/peek/views/tracing.rb new file mode 100644 index 00000000000..0de32a8fdda --- /dev/null +++ b/lib/peek/views/tracing.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Peek + module Views + class Tracing < View + def results + { + tracing_url: Gitlab::Tracing.tracing_url + } + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9ec590f90d8..8d7ed7a5b88 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 "" @@ -2489,6 +2501,9 @@ msgstr "" msgid "Data is still calculating..." msgstr "" +msgid "Date picker" +msgstr "" + msgid "Debug" msgstr "" @@ -2507,6 +2522,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 +2567,9 @@ msgstr "" msgid "Delete list" msgstr "" +msgid "Delete source branch" +msgstr "" + msgid "Delete this attachment" msgstr "" @@ -3172,6 +3196,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 +3310,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 +3325,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 +3402,9 @@ msgstr "" msgid "Finished" msgstr "" +msgid "First day of the week" +msgstr "" + msgid "FirstPushedBy|First" msgstr "" @@ -3915,15 +3965,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 +4362,9 @@ msgstr "" msgid "Loading…" msgstr "" +msgid "Localization" +msgstr "" + msgid "Lock" msgstr "" @@ -4447,9 +4515,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 +4536,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 "" @@ -4504,6 +4584,9 @@ msgstr "" msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}" msgstr "" +msgid "MergeRequest|Filter files" +msgstr "" + msgid "MergeRequest|No files found" msgstr "" @@ -4615,6 +4698,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 +5179,9 @@ msgstr "" msgid "Pages Domains" msgstr "" +msgid "Pages getting started guide" +msgstr "" + msgid "Pagination|Last »" msgstr "" @@ -6605,7 +6700,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 +6730,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 +6745,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 +6892,9 @@ msgstr "" msgid "Specify the following URL during the Runner setup:" msgstr "" +msgid "Squash commit message" +msgstr "" + msgid "Squash commits" msgstr "" @@ -6926,6 +7033,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 +7048,9 @@ msgstr "" msgid "System Info" msgstr "" +msgid "System default (%{default})" +msgstr "" + msgid "System metrics (Custom)" msgstr "" @@ -7977,6 +8090,9 @@ msgstr "" msgid "Various email settings." msgstr "" +msgid "Various localization settings." +msgstr "" + msgid "Various settings that affect GitLab performance." msgstr "" @@ -8304,6 +8420,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 +8717,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..c88ade87af6 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", @@ -38,7 +38,7 @@ "bootstrap": "4.1.3", "brace-expansion": "^1.1.8", "cache-loader": "^2.0.1", - "chart.js": "1.0.2", + "chart.js": "2.7.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", "codesandbox-api": "^0.0.20", @@ -62,6 +62,11 @@ module QA autoload :Fork, 'qa/resource/fork' autoload :SSHKey, 'qa/resource/ssh_key' + module Events + autoload :Base, 'qa/resource/events/base' + autoload :Project, 'qa/resource/events/project' + end + module Repository autoload :Push, 'qa/resource/repository/push' autoload :ProjectPush, 'qa/resource/repository/project_push' @@ -361,6 +366,7 @@ module QA autoload :Logging, 'qa/support/page/logging' end autoload :Api, 'qa/support/api' + autoload :Waiter, 'qa/support/waiter' end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index b1f27131207..69a8e691ceb 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -18,19 +18,10 @@ module QA page.refresh end - def wait(max: 60, time: 0.1, reload: true) - start = Time.now - - while Time.now - start < max - result = yield - return result if result - - sleep(time) - - refresh if reload + def wait(max: 60, interval: 0.1, reload: true) + QA::Support::Waiter.wait(max: max, interval: interval) do + yield || (reload && refresh && false) end - - false end def with_retry(max_attempts: 3, reload: false) @@ -73,7 +64,7 @@ module QA xhr.send(); JS - return false unless wait(time: 0.5, max: 60, reload: false) do + return false unless wait(interval: 0.5, max: 60, reload: false) do page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE') end diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb index d37b63c716a..b80877f5ecd 100644 --- a/qa/qa/page/component/clone_panel.rb +++ b/qa/qa/page/component/clone_panel.rb @@ -21,11 +21,6 @@ module QA repository_clone_location(:ssh_clone_url) end - def wait_for_push - sleep 5 - refresh - end - private def repository_clone_location(kind) diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb index 99132190f3f..e495cf4ef04 100644 --- a/qa/qa/page/component/legacy_clone_panel.rb +++ b/qa/qa/page/component/legacy_clone_panel.rb @@ -27,11 +27,6 @@ module QA Git::Location.new(find('#project_clone').value) end - def wait_for_push - sleep 5 - refresh - end - private def choose_repository_clone(kind, detect_text) 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/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index f54bea880a0..976e431186d 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -148,7 +148,7 @@ module QA end def add_comment_to_diff(text) - wait(time: 5) do + wait(interval: 5) do has_text?("No newline at end of file") end all_elements(:new_diff_line).first.hover diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index a588af07e4a..9f1867ef8a5 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -26,7 +26,7 @@ module QA def choose_test_namespace click_element :project_namespace_select - select_item(Runtime::Namespace.path) + search_and_select(Runtime::Namespace.path) end def go_to_import_project 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/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb index a73be7dfeda..831166f6373 100644 --- a/qa/qa/page/project/settings/mirroring_repositories.rb +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -62,7 +62,7 @@ module QA sleep 5 refresh - wait(time: 1) do + wait(interval: 1) do within_element_by_index(:mirrored_repository_row, row_index) do last_update = find_element(:mirror_last_update_at, wait: 0) last_update.has_text?('just now') || last_update.has_text?('seconds') diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 945b244df15..9c21d9ddbfa 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -51,6 +51,16 @@ module QA element :branches_dropdown end + view 'app/views/projects/blob/viewers/_loading.html.haml' do + element :spinner + end + + def wait_for_viewers_to_load + wait(reload: false) do + has_no_element?(:spinner) + end + end + def create_first_new_file! within_element(:quick_actions) do click_link_with_text 'New file' diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index ffe8633dd16..523d92c7ef3 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -27,6 +27,10 @@ module QA attributes.each(&method(:public_send)) end + def wait(max: 60, interval: 0.1) + QA::Support::Waiter.wait(max: max, interval: interval) + end + private def populate_attribute(name, block) diff --git a/qa/qa/resource/events/base.rb b/qa/qa/resource/events/base.rb new file mode 100644 index 00000000000..b50b620b143 --- /dev/null +++ b/qa/qa/resource/events/base.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module QA + module Resource + module Events + MAX_WAIT = 10 + + EventNotFoundError = Class.new(RuntimeError) + + module Base + def events(action: nil) + path = [api_get_events] + path << "?action=#{CGI.escape(action)}" if action + parse_body(api_get_from("#{path.join}")) + end + + private + + def api_get_events + "#{api_get_path}/events" + end + + def wait_for_event + event_found = QA::Support::Waiter.wait(max: max_wait) do + yield + end + + raise EventNotFoundError, "Timed out waiting for event" unless event_found + end + + def max_wait + MAX_WAIT + end + end + end + end +end diff --git a/qa/qa/resource/events/project.rb b/qa/qa/resource/events/project.rb new file mode 100644 index 00000000000..99c78254f42 --- /dev/null +++ b/qa/qa/resource/events/project.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Resource + module Events + module Project + include Events::Base + + def wait_for_push(commit_message) + QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_push with commit message "#{commit_message}"]) + wait_for_event do + events(action: 'pushed').any? { |event| event.dig(:push_data, :commit_title) == commit_message } + end + end + + def wait_for_push_new_branch(branch_name = "master") + QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_push_new_branch with branch_name "#{branch_name}"]) + wait_for_event do + events(action: 'pushed').any? { |event| event.dig(:push_data, :ref) == branch_name } + end + end + end + end + end +end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index c6243ff43fa..03bc1f0820b 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -5,12 +5,12 @@ module QA class Fork < Base attribute :project do Resource::Project.fabricate! do |resource| - resource.name = push.project.name - resource.path_with_namespace = "#{user.name}/#{push.project.name}" + resource.name = upstream.project.name + resource.path_with_namespace = "#{user.name}/#{upstream.project.name}" end end - attribute :push do + attribute :upstream do Repository::ProjectPush.fabricate! end @@ -24,7 +24,7 @@ module QA end def fabricate! - populate(:push, :user) + populate(:upstream, :user) # Sign out as admin and sign is as the fork user Page::Main::Menu.perform(&:sign_out) @@ -33,7 +33,7 @@ module QA login.sign_in_using_credentials(user) end - push.project.visit! + upstream.project.visit! Page::Project::Show.perform(&:fork_project) diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index a7a6f931e28..d7f9ec6a836 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -33,7 +33,7 @@ module QA end # Ensure that the group was actually created - group_show.wait(time: 1) do + group_show.wait(interval: 1) do group_show.has_text?(path) && group_show.has_new_project_or_subgroup_dropdown? 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/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 7150098a00a..45cb317e0eb 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -58,10 +58,7 @@ module QA populate(:target, :source) project.visit! - Page::Project::Show.perform do |project| - project.wait_for_push - project.new_merge_request - end + Page::Project::Show.perform(&:new_merge_request) Page::MergeRequest::New.perform do |page| page.fill_title(@title) page.fill_description(@description) diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 433e5a8f7c9..de1e9f04c36 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -5,6 +5,8 @@ require 'securerandom' module QA module Resource class Project < Base + include Events::Project + attribute :name attribute :description diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index f4692c3dd4d..cad89ebb0bb 100644 --- a/qa/qa/resource/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -4,6 +4,8 @@ module QA module Resource module Repository class ProjectPush < Repository::Push + attr_writer :wait_for_push + attribute :project do Project.fabricate! do |resource| resource.name = 'project-with-code' @@ -17,6 +19,7 @@ module QA @commit_message = "This is a test commit" @branch_name = 'master' @new_branch = true + @wait_for_push = true end def repository_http_uri @@ -30,6 +33,7 @@ module QA def fabricate! super project.visit! + project.wait_for_push @commit_message if @wait_for_push end end end 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..60836a7b98d 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) @@ -25,7 +26,6 @@ module QA push.file_content = "Test with unicode characters ❤✓€❄" end - Page::Project::Show.perform(&:wait_for_push) merge_request.visit! expect(page).to have_text('to be squashed') @@ -37,9 +37,7 @@ module QA merge_request.project.visit! Git::Repository.perform do |repository| - repository.uri = Page::Project::Show.act do - repository_clone_http_location.uri - end + repository.uri = merge_request.project.repository_http_location.uri repository.use_default_credentials diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb index 3567ddca1a1..8e181eb28c6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -23,7 +23,6 @@ module QA proj.name = 'project-qa-test' proj.description = 'project for qa test' end - project.visit! Git::Repository.perform do |repository| repository.uri = project.repository_http_location.uri @@ -53,7 +52,8 @@ module QA push_changes(third_branch) end end - Page::Project::Show.perform(&:wait_for_push) + project.wait_for_push commit_message_of_third_branch + project.visit! end it 'branches are correctly listed after CRUD operations' do diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 571cae4a3c5..f2584f55a60 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -3,22 +3,18 @@ module QA context 'Create' do describe 'Git clone over HTTP', :ldap_no_tls do - let(:location) do - Page::Project::Show.perform(&:repository_clone_http_location).uri - end - - before do + before(:all) do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - project = Resource::Project.fabricate! do |scenario| + @project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end - project.visit! + @project.visit! Git::Repository.perform do |repository| - repository.uri = location + repository.uri = @project.repository_http_location.uri repository.use_default_credentials repository.act do @@ -29,12 +25,12 @@ module QA push_changes end end - Page::Project::Show.perform(&:wait_for_push) + @project.wait_for_push_new_branch end it 'user performs a deep clone' do Git::Repository.perform do |repository| - repository.uri = location + repository.uri = @project.repository_http_location.uri repository.use_default_credentials repository.clone @@ -45,7 +41,7 @@ module QA it 'user performs a shallow clone' do Git::Repository.perform do |repository| - repository.uri = location + repository.uri = @project.repository_http_location.uri repository.use_default_credentials repository.shallow_clone diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb index 43894372cf5..d1535d6519d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -35,7 +35,7 @@ module QA end project.visit! - Page::Project::Show.perform(&:wait_for_push) + project.wait_for_push_new_branch # Check that the push worked expect(page).to have_content(file_name) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb index 135925c007f..48800cc81e5 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb @@ -70,7 +70,7 @@ module QA end project.visit! - Page::Project::Show.perform(&:wait_for_push) + project.wait_for_push_new_branch # Check that the push worked expect(page).to have_content(file_name) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 3310a873a60..1f4fb08accc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -22,7 +22,8 @@ module QA end push.project.visit! - Page::Project::Show.perform(&:wait_for_push) + + Page::Project::Show.perform(&:wait_for_viewers_to_load) expect(page).to have_content('README.md') expect(page).to have_content('This is a test project') diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb index 9d31a25ab35..2e4915b898d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -20,8 +20,6 @@ module QA end source_project_push.project.visit! - Page::Project::Show.perform(&:wait_for_push) - Page::Project::Menu.perform(&:click_repository_settings) Page::Project::Settings::Repository.perform do |settings| settings.expand_mirroring_repositories do |mirror_settings| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb index 23ea55c2e61..7d96da32423 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb @@ -31,7 +31,7 @@ module QA set_file_size_limit(5) expect(page).to have_content("Application settings saved successfully") - push = push_new_file('oversize_file_1.bin') + push = push_new_file('oversize_file_1.bin', wait_for_push: true) expect(push.output).not_to have_content 'remote: fatal: pack exceeds maximum allowed size' end @@ -39,7 +39,7 @@ module QA set_file_size_limit(1) expect(page).to have_content("Application settings saved successfully") - push = push_new_file('oversize_file_2.bin') + push = push_new_file('oversize_file_2.bin', wait_for_push: false) expect(push.output).to have_content 'remote: fatal: pack exceeds maximum allowed size' end @@ -55,7 +55,7 @@ module QA end end - def push_new_file(file_name) + def push_new_file(file_name, wait_for_push: true) @project.visit! Resource::Repository::ProjectPush.fabricate! do |p| @@ -63,6 +63,7 @@ module QA p.file_name = file_name p.file_content = SecureRandom.random_bytes(2000000) p.commit_message = 'Adding a new file' + p.wait_for_push = wait_for_push end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index d10ad896b3b..58e6c160a3a 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -5,7 +5,7 @@ module QA describe 'Git push over HTTP', :ldap_no_tls do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) project_push = Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' @@ -13,7 +13,6 @@ module QA push.commit_message = 'Add README.md' end project_push.project.visit! - Page::Project::Show.act { wait_for_push } expect(page).to have_content('README.md') expect(page).to have_content('This is a test project') diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index 73a3dc14a65..4464fb812b7 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -63,6 +63,7 @@ module QA resource.commit_message = 'Add new_file.md' resource.branch_name = branch_name resource.new_branch = false + resource.wait_for_push = false end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 509a639c130..7223831d96f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -10,7 +10,7 @@ module QA it 'user adds an ssh key and pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title @@ -24,13 +24,12 @@ module QA end project_push.project.visit! - Page::Project::Show.act { wait_for_push } expect(page).to have_content('README.md') expect(page).to have_content('Test Use SSH Key') - Page::Main::Menu.act { go_to_profile_settings } - Page::Profile::Menu.act { click_ssh_keys } + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key_title) 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/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index 25cbe41c684..2238d6c382e 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -11,7 +11,7 @@ module QA it 'users creates a pipeline which gets processed' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) project = Resource::Project.fabricate! do |project| project.name = 'project-with-pipelines' @@ -60,11 +60,9 @@ module QA EOF end - Page::Project::Show.act { wait_for_push } - expect(page).to have_content('Add .gitlab-ci.yml') - Page::Project::Menu.act { click_ci_cd_pipelines } + Page::Project::Menu.perform(&:click_ci_cd_pipelines) expect(page).to have_content('All 1') expect(page).to have_content('Add .gitlab-ci.yml') @@ -72,7 +70,7 @@ module QA puts 'Waiting for the runner to process the pipeline' sleep 15 # Runner should process all jobs within 15 seconds. - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline) Page::Project::Pipeline::Show.perform do |pipeline| expect(pipeline).to be_running 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..2375aa4ce91 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,11 +3,12 @@ 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) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) end before(:all) do @@ -28,7 +29,7 @@ module QA resource.image = 'gitlab/gitlab-runner:ubuntu' end - Page::Main::Menu.act { sign_out } + Page::Main::Menu.perform(&:sign_out) end after(:all) do @@ -89,10 +90,9 @@ module QA sha1sum = Digest::SHA1.hexdigest(gitlab_ci) - Page::Project::Show.act { wait_for_push } - Page::Project::Menu.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } - Page::Project::Pipeline::Show.act { go_to_first_job } + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline) + Page::Project::Pipeline::Show.perform(&:go_to_first_job) Page::Project::Job::Show.perform do |job| expect(job).to be_successful, "Job status did not become \"passed\"." 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..8cd353fa250 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 @@ -8,7 +8,7 @@ module QA describe 'Auto DevOps support' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) end [true, false].each do |rbac| @@ -38,8 +38,6 @@ module QA push.commit_message = 'Create Auto DevOps compatible rack application' end - Page::Project::Show.act { wait_for_push } - # Create and connect K8s cluster @cluster = Service::KubernetesCluster.new(rbac: rbac).create! kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| @@ -52,13 +50,13 @@ module QA end kubernetes_cluster.populate(:ingress_ip) - @project.visit! - Page::Project::Menu.act { click_ci_cd_settings } + Page::Project::Menu.perform(&: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 @@ -71,8 +69,8 @@ module QA it 'runs auto devops' do @project.visit! - Page::Project::Menu.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline) Page::Project::Pipeline::Show.perform do |pipeline| pipeline.go_to_job('build') @@ -101,7 +99,7 @@ module QA job.click_element(:pipeline_path) end - Page::Project::Menu.act { click_operations_environments } + Page::Project::Menu.perform(&:click_operations_environments) Page::Project::Operations::Environments::Index.perform do |index| index.go_to_environment('production') end @@ -132,8 +130,8 @@ module QA end @project.visit! - Page::Project::Menu.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline) Page::Project::Pipeline::Show.perform do |pipeline| pipeline.go_to_job('build') @@ -162,7 +160,7 @@ module QA job.click_element(:pipeline_path) end - Page::Project::Menu.act { click_operations_environments } + Page::Project::Menu.perform(&:click_operations_environments) Page::Project::Operations::Environments::Index.perform do |index| index.go_to_environment('production') diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index f2cd0194b6b..5e97a92a6e3 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -10,15 +10,11 @@ module QA super end - def wait(max: 60, time: 0.1, reload: true) - log("with wait: max #{max}; time #{time}; reload #{reload}") - now = Time.now - - element = super + def wait(max: 60, interval: 0.1, reload: true) + log("next wait uses reload: #{reload}") + # Logging of wait start/end/duration is handled by QA::Support::Waiter - log("ended wait after #{Time.now - now} seconds") - - element + super end def scroll_to(selector, text: nil) diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb new file mode 100644 index 00000000000..21a399b4a5f --- /dev/null +++ b/qa/qa/support/waiter.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Support + module Waiter + module_function + + def wait(max: 60, interval: 0.1) + QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}") + start = Time.now + + while Time.now - start < max + result = yield + if result + log_end(Time.now - start) + return result + end + + sleep(interval) + end + log_end(Time.now - start) + + false + end + + def self.log_end(duration) + QA::Runtime::Logger.debug("ended wait after #{duration} seconds") + end + end + end +end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index 4a350cd6c42..62c81050bd9 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -1,5 +1,5 @@ describe QA::Git::Repository do - include Support::StubENV + include Helpers::StubENV shared_context 'git directory' do let(:repository) { described_class.new } diff --git a/qa/spec/support/stub_env.rb b/qa/spec/helpers/stub_env.rb index 4788e0ab46c..62b4a1df787 100644 --- a/qa/spec/support/stub_env.rb +++ b/qa/spec/helpers/stub_env.rb @@ -1,5 +1,5 @@ # Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb -module Support +module Helpers module StubENV def stub_env(key_or_hash, value = nil) init_stub unless env_stubbed? diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 076a8087db5..32a350f2154 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -59,4 +59,34 @@ describe QA::Page::Base do end end end + + describe '#wait' do + subject { Class.new(described_class).new } + + context 'when the condition is true' do + it 'does not refresh' do + expect(subject).not_to receive(:refresh) + + subject.wait(max: 0.01) { true } + end + + it 'returns true' do + expect(subject.wait(max: 0.1) { true }).to be_truthy + end + end + + context 'when the condition is false' do + it 'refreshes' do + expect(subject).to receive(:refresh).at_least(:once) + + subject.wait(max: 0.01) { false } + end + + it 'returns false' do + allow(subject).to receive(:refresh) + + expect(subject.wait(max: 0.01) { false }).to be_falsey + end + end + end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index f289ee3c2bb..a6e9601cee4 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -4,8 +4,6 @@ require 'capybara/dsl' require 'logger' describe QA::Support::Page::Logging do - include Support::StubENV - let(:page) { double.as_null_object } before do @@ -31,11 +29,22 @@ describe QA::Support::Page::Logging do it 'logs wait' do expect { subject.wait(max: 0) {} } + .to output(/next wait uses reload: true/).to_stdout_from_any_process + expect { subject.wait(max: 0) {} } .to output(/with wait/).to_stdout_from_any_process expect { subject.wait(max: 0) {} } .to output(/ended wait after .* seconds$/).to_stdout_from_any_process end + it 'logs wait with reload false' do + expect { subject.wait(max: 0, reload: false) {} } + .to output(/next wait uses reload: false/).to_stdout_from_any_process + expect { subject.wait(max: 0, reload: false) {} } + .to output(/with wait/).to_stdout_from_any_process + expect { subject.wait(max: 0, reload: false) {} } + .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + end + it 'logs scroll_to' do expect { subject.scroll_to(:element) } .to output(/scrolling to :element/).to_stdout_from_any_process diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index a2a3ad01749..4a6b76c869f 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe QA::Resource::Base do - include Support::StubENV + include Helpers::StubENV let(:resource) { spy('resource') } let(:location) { 'http://location' } diff --git a/qa/spec/resource/events/base_spec.rb b/qa/spec/resource/events/base_spec.rb new file mode 100644 index 00000000000..9cdf4785092 --- /dev/null +++ b/qa/spec/resource/events/base_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +describe QA::Resource::Events::Base do + let(:resource) do + Class.new(QA::Resource::Base) do + def api_get_path + '/foo' + end + end + end + + subject { resource.tap { |f| f.include(described_class) }.new } + + describe "#events" do + it 'fetches all events when called without parameters' do + allow(subject).to receive(:parse_body).and_return('returned') + + expect(subject).to receive(:api_get_from).with('/foo/events') + expect(subject.events).to eq('returned') + end + + it 'fetches events with a specified action type' do + allow(subject).to receive(:parse_body).and_return('returned') + + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed') + expect(subject.events(action: 'pushed')).to eq('returned') + end + end +end diff --git a/qa/spec/resource/events/project_spec.rb b/qa/spec/resource/events/project_spec.rb new file mode 100644 index 00000000000..b3efdb518f3 --- /dev/null +++ b/qa/spec/resource/events/project_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +describe QA::Resource::Events::Project do + let(:resource) do + Class.new(QA::Resource::Base) do + def api_get_path + '/foo' + end + end + end + let(:all_events) do + [ + { + "action_name": "pushed", + "push_data": { + "commit_title": "foo commit" + } + }, + { + "action_name": "pushed", + "push_data": { + "ref": "master" + } + }, + { + "action_name": "pushed", + "push_data": { + "ref": "another-branch" + } + } + ] + end + + before do + allow(subject).to receive(:max_wait).and_return(0.01) + allow(subject).to receive(:parse_body).and_return(all_events) + end + + subject { resource.tap { |f| f.include(described_class) }.new } + + describe "#wait_for_push" do + it 'waits for a push with a specified commit message' do + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed') + expect { subject.wait_for_push('foo commit') }.not_to raise_error + end + + it 'raises an error if a push with the specified commit message is not found' do + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed').at_least(:once) + expect { subject.wait_for_push('bar') }.to raise_error(QA::Resource::Events::EventNotFoundError) + end + end + + describe "#wait_for_push_new_branch" do + it 'waits for a push to master if no branch is given' do + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed') + expect { subject.wait_for_push_new_branch }.not_to raise_error + end + + it 'waits for a push to the given branch' do + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed') + expect { subject.wait_for_push_new_branch('another-branch') }.not_to raise_error + end + + it 'raises an error if a push with the specified branch is not found' do + expect(subject).to receive(:api_get_from).with('/foo/events?action=pushed').at_least(:once) + expect { subject.wait_for_push_new_branch('bar') }.to raise_error(QA::Resource::Events::EventNotFoundError) + end + end +end diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb index 975586b505f..af43facc73d 100644 --- a/qa/spec/runtime/api/client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb @@ -1,5 +1,5 @@ describe QA::Runtime::API::Client do - include Support::StubENV + include Helpers::StubENV describe 'initialization' do it 'defaults to :gitlab address' do diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index bc0ec08d66d..fc51f45c3a1 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true describe QA::Runtime::Env do - include Support::StubENV + include Helpers::StubENV shared_examples 'boolean method' do |**kwargs| it_behaves_like 'boolean method with parameter', kwargs diff --git a/qa/spec/support/shared_examples/scenario_shared_examples.rb b/qa/spec/shared_examples/scenario_shared_examples.rb index 5fd55d7d96b..5fd55d7d96b 100644 --- a/qa/spec/support/shared_examples/scenario_shared_examples.rb +++ b/qa/spec/shared_examples/scenario_shared_examples.rb diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 3537ba7c235..3e7a6dd26ee 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -1,8 +1,24 @@ require_relative '../qa' -Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } +%w[helpers shared_examples].each do |d| + Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f } +end 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/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb new file mode 100644 index 00000000000..8283b65e1be --- /dev/null +++ b/qa/spec/support/waiter_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'logger' + +describe QA::Support::Waiter do + before do + logger = ::Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + end + + describe '.wait' do + context 'when the condition is true' do + it 'logs the start' do + expect { subject.wait(max: 0) {} } + .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.wait(max: 0) {} } + .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + end + end + + context 'when the condition is false' do + it 'logs the start' do + expect { subject.wait(max: 0) { false } } + .to output(/with wait: max 0; interval 0.1/).to_stdout_from_any_process + end + + it 'logs the end' do + expect { subject.wait(max: 0) { false } } + .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + end + end + end +end 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/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/fixtures/clusters/sample_cert.pem b/spec/fixtures/clusters/sample_cert.pem index e39a2b34416..00e6ce44d87 100644 --- a/spec/fixtures/clusters/sample_cert.pem +++ b/spec/fixtures/clusters/sample_cert.pem @@ -30,4 +30,4 @@ TkIdFE47ZisEDhIdF6wC1izEMLeMEsPAO7/Y6MY4nRxsinSe95lRaw+yQpzx+mvJ Q7n1kiHI9Pd5M3+CiQda0d/GO1o5ORJnUGJRvr9HKuNmE7Lif0As/N0AlywjzE7A 6Z8AEiWyRV1ffshu1k2UKmzvZuZeGGKRtrIjbJIRAtpRVtVZZGzhq5/sojCLoJ+u texqFBUo/4mFRZa4pDItUdyOlDy2/LO/ag== ------END CERTIFICATE----- +-----END CERTIFICATE-----
\ No newline at end of file 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/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index caf279edc97..cf9e0cccc71 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -20,6 +20,8 @@ describe Gitlab::Git::Repository, :seed_helper do end 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) } @@ -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 @@ -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/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/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/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb new file mode 100644 index 00000000000..2cddc895026 --- /dev/null +++ b/spec/lib/gitlab/tracing_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::Tracing do + using RSpec::Parameterized::TableSyntax + + describe '.enabled?' do + where(:connection_string, :enabled_state) do + nil | false + "" | false + "opentracing://jaeger" | true + end + + with_them do + it 'should return the correct state for .enabled?' do + expect(described_class).to receive(:connection_string).and_return(connection_string) + + expect(described_class.enabled?).to eq(enabled_state) + end + end + end + + describe '.tracing_url_enabled?' do + where(:enabled?, :tracing_url_template, :tracing_url_enabled_state) do + false | nil | false + false | "" | false + false | "http://localhost" | false + true | nil | false + true | "" | false + true | "http://localhost" | true + end + + with_them do + it 'should return the correct state for .tracing_url_enabled?' do + expect(described_class).to receive(:enabled?).and_return(enabled?) + allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) + + expect(described_class.tracing_url_enabled?).to eq(tracing_url_enabled_state) + end + end + end + + describe '.tracing_url' do + where(:tracing_url_enabled?, :tracing_url_template, :correlation_id, :process_name, :tracing_url) do + false | "https://localhost" | "123" | "web" | nil + true | "https://localhost" | "123" | "web" | "https://localhost" + true | "https://localhost?service=%{service}" | "123" | "web" | "https://localhost?service=web" + true | "https://localhost?c=%{correlation_id}" | "123" | "web" | "https://localhost?c=123" + true | "https://localhost?c=%{correlation_id}&s=%{service}" | "123" | "web" | "https://localhost?c=123&s=web" + true | "https://localhost?c=%{correlation_id}" | nil | "web" | "https://localhost?c=" + end + + with_them do + it 'should return the correct state for .tracing_url' do + expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?) + allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template) + allow(Gitlab::CorrelationId).to receive(:current_id).and_return(correlation_id) + allow(Gitlab).to receive(:process_name).and_return(process_name) + + expect(described_class.tracing_url).to eq(tracing_url) + end + end + 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/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb index 235e2ee4e69..b865909c7fd 100644 --- a/spec/models/clusters/kubernetes_namespace_spec.rb +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -97,7 +97,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do let(:platform) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) } let(:api_url) { 'https://kube.domain.com' } - let(:ca_pem) { 'CA PEM DATA' } + let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) } let(:token) { 'token' } let(:kubeconfig) do diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index 6c8a223092e..4068d98d8f7 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -114,6 +114,36 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end + context 'ca_cert' do + let(:kubernetes) { build(:cluster_platform_kubernetes, ca_pem: ca_pem) } + + context 'with a valid certificate' do + let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) } + + it { is_expected.to be_truthy } + end + + context 'with an invalid certificate' do + let(:ca_pem) { "invalid" } + + it { is_expected.to be_falsey } + + context 'but the certificate is not being updated' do + before do + allow(kubernetes).to receive(:ca_cert_changed?).and_return(false) + end + + it { is_expected.to be_truthy } + end + end + + context 'with no certificate' do + let(:ca_pem) { "" } + + it { is_expected.to be_truthy } + end + end + describe 'when using reserved namespaces' do subject { build(:cluster_platform_kubernetes, namespace: namespace) } @@ -202,7 +232,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) } let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem) } let(:api_url) { 'https://kube.domain.com' } - let(:ca_pem) { 'CA PEM DATA' } + let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) } subject { kubernetes.predefined_variables(project: cluster.project) } @@ -297,6 +327,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/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/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/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/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..c96a65cb56a 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,35 +48,35 @@ shared_examples 'cluster application status specs' do |application_name| expect(subject.version).to eq(subject.class.const_get(:VERSION)) end - end - describe '#make_updated' do - subject { create(application_name, :updating) } + context 'application is updating' do + subject { create(application_name, :updating) } - it 'is updated' do - subject.make_updated! + it 'is updated' do + subject.make_installed! - expect(subject).to be_updated - end + expect(subject).to be_updated + end - it 'updates helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') + it 'updates helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') - subject.make_updated! + subject.make_installed! - subject.cluster.application_helm.reload + subject.cluster.application_helm.reload - expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end + expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) + end - it 'updates the version for the application' do - subject.update!(version: '0.0.0') + it 'updates the version of the application' do + subject.update!(version: '0.0.0') - subject.make_updated! + subject.make_installed! - subject.reload + subject.reload - expect(subject.version).to eq(subject.class.const_get(:VERSION)) + expect(subject.version).to eq(subject.class.const_get(:VERSION)) + end end end @@ -90,6 +90,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 +123,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/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb new file mode 100644 index 00000000000..02c225292ce --- /dev/null +++ b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'projects/issues/_merge_requests_status.html.haml' do + it 'shows date of status change in tooltip' do + merge_request = create(:merge_request, created_at: 1.month.ago) + + render partial: 'projects/issues/merge_requests_status', + locals: { merge_request: merge_request, css_class: '' } + + expect(rendered).to match("Opened.*about 1 month ago") + end + + it 'shows only status in tooltip if date is not set' do + merge_request = create(:merge_request, state: :closed) + + render partial: 'projects/issues/merge_requests_status', + locals: { merge_request: merge_request, css_class: '' } + + expect(rendered).to match("Closed") + 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/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..8bff9c59113 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" @@ -2122,10 +2122,28 @@ chardet@^0.5.0: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= -chart.js@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-1.0.2.tgz#ad57d2229cfd8ccf5955147e8121b4911e69dfe7" - integrity sha1-rVfSIpz9jM9ZVRR+gSG0kR5p3+c= +chart.js@2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714" + integrity sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA== + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + +chartjs-color-string@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1" + integrity sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ== + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae" + integrity sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4= + dependencies: + chartjs-color-string "^0.5.0" + color-convert "^0.5.3" check-types@^7.3.0: version "7.3.0" @@ -2296,6 +2314,11 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" +color-convert@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2303,7 +2326,7 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" -color-name@1.1.3: +color-name@1.1.3, color-name@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= @@ -6985,10 +7008,10 @@ mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: dependencies: minimist "0.0.8" -moment@2.x, moment@^2.21.0: - version "2.22.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" - integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= +moment@2.x, moment@^2.10.2, moment@^2.21.0: + version "2.23.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" + integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA== monaco-editor-webpack-plugin@^1.7.0: version "1.7.0" |