diff options
author | Achilleas Pipinellis <axil@gitlab.com> | 2019-08-21 11:24:55 +0200 |
---|---|---|
committer | Achilleas Pipinellis <axil@gitlab.com> | 2019-08-21 11:24:55 +0200 |
commit | a39228db8027c966e8117d40766b0cef4fbd50f6 (patch) | |
tree | 143045af7214ae6746e7b7c9df647c71ec2bd675 /app | |
parent | 05f50c9b52fde54513fe55fef97499b35719eae2 (diff) | |
parent | aed489bf901745ced6618e680913d0d213998923 (diff) | |
download | gitlab-ce-docs-zm002-gitlab-case.tar.gz |
Merge branch 'master' into docs-zm002-gitlab-casedocs-zm002-gitlab-case
Diffstat (limited to 'app')
97 files changed, 726 insertions, 337 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue new file mode 100644 index 00000000000..d946594a069 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue @@ -0,0 +1,41 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton } from '@gitlab/ui'; + +export default { + name: 'StageCardListItem', + components: { + Icon, + GlButton, + }, + props: { + isActive: { + type: Boolean, + required: true, + }, + canEdit: { + type: Boolean, + default: false, + required: false, + }, + }, +}; +</script> + +<template> + <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded"> + <slot></slot> + <div v-if="canEdit" class="dropdown"> + <gl-button + :title="__('More actions')" + class="more-actions-toggle btn btn-transparent p-0" + data-toggle="dropdown" + > + <icon css-classes="icon" name="ellipsis_v" /> + </gl-button> + <ul class="more-actions-dropdown dropdown-menu dropdown-open-left"> + <slot name="dropdown-options"></slot> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue new file mode 100644 index 00000000000..004d335f572 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue @@ -0,0 +1,88 @@ +<script> +import StageCardListItem from './stage_card_list_item.vue'; + +export default { + name: 'StageNavItem', + components: { + StageCardListItem, + }, + props: { + isDefaultStage: { + type: Boolean, + default: false, + required: false, + }, + isActive: { + type: Boolean, + default: false, + required: false, + }, + isUserAllowed: { + type: Boolean, + required: true, + }, + title: { + type: String, + required: true, + }, + value: { + type: String, + default: '', + required: false, + }, + canEdit: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + hasValue() { + return this.value && this.value.length > 0; + }, + editable() { + return this.isUserAllowed && this.canEdit; + }, + }, +}; +</script> + +<template> + <li @click="$emit('select')"> + <stage-card-list-item :is-active="isActive" :can-edit="editable"> + <div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }"> + {{ title }} + </div> + <div class="stage-nav-item-cell stage-median mr-4"> + <template v-if="isUserAllowed"> + <span v-if="hasValue">{{ value }}</span> + <span v-else class="stage-empty">{{ __('Not enough data') }}</span> + </template> + <template v-else> + <span class="not-available">{{ __('Not available') }}</span> + </template> + </div> + <template v-slot:dropdown-options> + <template v-if="isDefaultStage"> + <li> + <button type="button" class="btn-default btn-transparent"> + {{ __('Hide stage') }} + </button> + </li> + </template> + <template v-else> + <li> + <button type="button" class="btn-default btn-transparent"> + {{ __('Edit stage') }} + </button> + </li> + <li> + <button type="button" class="btn-danger danger"> + {{ __('Remove stage') }} + </button> + </li> + </template> + </template> + </stage-card-list-item> + </li> +</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 671405602cc..b3ae47af750 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -12,6 +12,7 @@ import stageComponent from './components/stage_component.vue'; import stageReviewComponent from './components/stage_review_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue'; import stageTestComponent from './components/stage_test_component.vue'; +import stageNavItem from './components/stage_nav_item.vue'; import CycleAnalyticsService from './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store'; @@ -41,6 +42,7 @@ export default () => { import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'), DateRangeDropdown: () => import('ee_component/analytics/shared/components/date_range_dropdown.vue'), + 'stage-nav-item': stageNavItem, }, mixins: [filterMixins], data() { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 81da0754752..19b85710710 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -305,7 +305,7 @@ export default { <div v-show="showTreeList" :style="{ width: `${treeWidth}px` }" - class="diff-tree-list js-diff-tree-list" + class="diff-tree-list js-diff-tree-list mr-3" > <panel-resizer :size.sync="treeWidth" diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue index 925385fa98a..839ab542377 100644 --- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue @@ -212,19 +212,18 @@ export default { </script> <template> - <td :colspan="colspan"> + <td :colspan="colspan" class="text-center"> <div class="content js-line-expansion-content"> <a v-if="canExpandUp" v-tooltip - class="cursor-pointer js-unfold unfold-icon" + class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2" data-placement="top" data-container="body" :title="__('Expand up')" @click="handleExpandLines(EXPAND_UP)" > - <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 --> - <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(270deg);" /> + <icon :size="12" name="expand-up" aria-hidden="true" /> </a> <a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()"> <span>{{ s__('Diffs|Show all lines') }}</span> @@ -232,14 +231,13 @@ export default { <a v-if="canExpandDown" v-tooltip - class="cursor-pointer js-unfold-down has-tooltip unfold-icon" + class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2" data-placement="top" data-container="body" :title="__('Expand down')" @click="handleExpandLines(EXPAND_DOWN)" > - <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 --> - <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(90deg);" /> + <icon :size="12" name="expand-down" aria-hidden="true" /> </a> </div> </td> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 32fbeaaa905..69ec6ab8600 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -130,7 +130,7 @@ export default { return `\`${this.diffFile.file_path}\``; }, isFileRenamed() { - return this.diffFile.viewer.name === diffViewerModes.renamed; + return this.diffFile.renamed_file; }, isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 80a6ab9598a..7254c50a568 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -87,7 +87,6 @@ export default { :file="file" :show-tooltip="true" :show-staged-icon="true" - :force-modified-icon="true" /> <new-dropdown :type="file.type" diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 04e86afb268..52200ce7847 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -129,7 +129,7 @@ export const commitActionForFile = file => { export const getCommitFiles = stagedFiles => stagedFiles.reduce((acc, file) => { - if (file.moved) return acc; + if (file.moved || file.type === 'tree') return acc; return acc.concat({ ...file, diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 1336b6a5461..7ead9d46fbb 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,5 +1,11 @@ import { join as joinPaths } from 'path'; +// Returns a decoded url parameter value +// - Treats '+' as '%20' +function decodeUrlParameter(val) { + return decodeURIComponent(val.replace(/\+/g, '%20')); +} + // Returns an array containing the value(s) of the // of the key passed as an argument export function getParameterValues(sParam, url = window.location) { @@ -30,7 +36,7 @@ export function mergeUrlParams(params, url) { .forEach(part => { if (part.length) { const kv = part.split('='); - merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('=')); + merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('=')); } }); } diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index af2697444f2..d719fd8748d 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -17,6 +17,8 @@ export default class Members { } dropdownClicked(options) { + options.e.preventDefault(); + this.formSubmit(null, options.$el); } diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 838447e6c75..90c764587a3 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -45,6 +45,11 @@ export default { required: false, default: () => false, }, + singleEmbed: { + type: Boolean, + required: false, + default: false, + }, thresholds: { type: Array, required: false, @@ -240,7 +245,10 @@ export default { </script> <template> - <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div + class="prometheus-graph col-12" + :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]" + > <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 587392adbc3..ebd610af7b6 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -10,13 +10,13 @@ import { } from '@gitlab/ui'; import _ from 'underscore'; import { mapActions, mapState } from 'vuex'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import { getParameterValues } from '~/lib/utils/url_utility'; +import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; +import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import MonitorAreaChart from './charts/area.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; -import PanelType from './panel_type.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; import { sidebarAnimationDuration, timeWindows } from '../constants'; @@ -141,6 +141,16 @@ export default { required: false, default: false, }, + alertsEndpoint: { + type: String, + required: false, + default: null, + }, + prometheusAlertsAvailable: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -168,8 +178,11 @@ export default { 'multipleDashboardsEnabled', 'additionalPanelTypesEnabled', ]), + firstDashboard() { + return this.allDashboards[0] || {}; + }, selectedDashboardText() { - return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); + return this.currentDashboard || this.firstDashboard.display_name; }, addingMetricsAvailable() { return IS_EE && this.canAddMetrics && !this.showEmptyState; @@ -258,7 +271,15 @@ export default { getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, // TODO: END + generateLink(group, title, yLabel) { + const dashboard = this.currentDashboard || this.firstDashboard.path; + const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null); + return mergeUrlParams(params, window.location.href); + }, hideAddMetricModal() { this.$refs.addMetricModal.hide(); }, @@ -435,8 +456,11 @@ export default { <panel-type v-for="(graphData, graphIndex) in groupData.metrics" :key="`panel-type-${graphIndex}`" + :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :graph-data="graphData" :dashboard-width="elWidth" + :alerts-endpoint="alertsEndpoint" + :prometheus-alerts-available="prometheusAlertsAvailable" :index="`${index}-${graphIndex}`" /> </template> @@ -475,6 +499,15 @@ export default { {{ __('Download CSV') }} </gl-dropdown-item> <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text=" + generateLink(groupData.group, graphData.title, graphData.y_label) + " + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> + <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}-${graphIndex}`" > diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index 9e85b0633fe..e3256147618 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -36,12 +36,15 @@ export default { }, computed: { ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), - groupData() { - const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length); - if (groupsWithData.length) { - return groupsWithData[0]; - } - return null; + charts() { + const groupWithMetrics = this.groups.find(group => + group.metrics.find(chart => this.chartHasData(chart)), + ) || { metrics: [] }; + + return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart)); + }, + isSingleChart() { + return this.charts.length === 1; }, }, mounted() { @@ -66,10 +69,8 @@ export default { 'setFeatureFlags', 'setShowErrorBanner', ]), - chartsWithData(charts) { - return charts.filter(chart => - chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), - ); + chartHasData(chart) { + return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)); }, onSidebarMutation() { setTimeout(() => { @@ -89,16 +90,17 @@ export default { }; </script> <template> - <div class="metrics-embed"> - <div v-if="groupData" class="row w-100 m-n2 pb-4"> + <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }"> + <div v-if="charts.length" class="row w-100 m-n2 pb-4"> <monitor-area-chart - v-for="graphData in chartsWithData(groupData.metrics)" + v-for="graphData in charts" :key="graphData.title" :graph-data="graphData" :container-width="elWidth" group-id="monitor-area-chart" :project-path="null" :show-border="true" + :single-embed="isSingleChart" /> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 3fbac71f3d7..96f62bc85ee 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -1,6 +1,7 @@ <script> import { mapState } from 'vuex'; import _ from 'underscore'; +import { __ } from '~/locale'; import { GlDropdown, GlDropdownItem, @@ -28,6 +29,10 @@ export default { GlTooltip: GlTooltipDirective, }, props: { + clipboardText: { + type: String, + required: true, + }, graphData: { type: Object, required: true, @@ -76,6 +81,9 @@ export default { isPanelType(type) { return this.graphData.type && this.graphData.type === type; }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, }, }; </script> @@ -116,6 +124,13 @@ export default { <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> {{ __('Download CSV') }} </gl-dropdown-item> + <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text="clipboardText" + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> {{ __('Alerts') }} </gl-dropdown-item> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c0fee1ebb99..51cef20455c 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,9 +1,12 @@ import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue'; import store from './stores'; +Vue.use(GlToast); + export default (props = {}) => { const el = document.getElementById('prometheus-graphs'); diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index 8caac68e0d4..622db360d1f 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -59,6 +59,10 @@ export default () => { render(createElement) { const isDiffView = this.activeTab === 'diffs'; + // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`, + // it adds a global key listener so it works on the diffs tab as well. + // If we create a single Vue app for all of the MR tabs, we should move this + // up the tree, to the root. return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [ createElement('notes-app', { props: { diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue index 5fc2b6ba04c..7fbfe8eebb2 100644 --- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue @@ -25,6 +25,10 @@ export default { Mousetrap.bind('n', () => this.jumpToNextDiscussion()); Mousetrap.bind('p', () => this.jumpToPreviousDiscussion()); }, + beforeDestroy() { + Mousetrap.unbind('n'); + Mousetrap.unbind('p'); + }, methods: { ...mapActions(['expandDiscussion']), jumpToNextDiscussion() { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 627d37bac68..a223a8f5b08 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + canDisableEmails: { + type: Boolean, + required: false, + default: false, + }, canChangeVisibilityLevel: { type: Boolean, required: false, @@ -104,6 +109,7 @@ export default { lfsEnabled: true, requestAccessEnabled: true, highlightChangesClass: false, + emailsDisabled: false, }; return { ...defaults, ...this.currentSettings }; @@ -341,5 +347,14 @@ export default { /> </project-setting-row> </div> + <project-setting-row v-if="canDisableEmails" class="mb-3"> + <label class="js-emails-disabled"> + <input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" /> + <input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }} + </label> + <span class="form-text text-muted">{{ + __('This setting will override user notification preferences for all project members.') + }}</span> + </project-setting-row> </div> </template> diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js index 9b58d42b47d..d41199f6374 100644 --- a/app/assets/javascripts/pages/projects/wikis/wikis.js +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -1,6 +1,5 @@ import bp from '../../../breakpoints'; -import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils'; -import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility'; +import { s__, sprintf } from '~/locale'; export default class Wikis { constructor() { @@ -12,32 +11,37 @@ export default class Wikis { sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); } - this.newWikiForm = document.querySelector('form.new-wiki-page'); - if (this.newWikiForm) { - this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page')); + this.editTitleInput = document.querySelector('form.wiki-form #wiki_title'); + this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message'); + this.commitMessageI18n = this.isNewWikiPage + ? s__('WikiPageCreate|Create %{pageTitle}') + : s__('WikiPageEdit|Update %{pageTitle}'); + + if (this.editTitleInput) { + // Initialize the commit message on load + if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value); + + // Set the commit message as the page title is changed + this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e)); } window.addEventListener('resize', () => this.renderSidebar()); this.renderSidebar(); } - handleNewWikiSubmit(e) { - if (!this.newWikiForm) return; - - const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - - const slug = slugInput.value; + handleWikiTitleChange(e) { + this.setWikiCommitMessage(e.target.value); + } - if (slug.length > 0) { - const wikisPath = slugInput.getAttribute('data-wikis-path'); + setWikiCommitMessage(rawTitle) { + let title = rawTitle; - // If the wiki is empty, we need to merge the current URL params to keep the "create" view. - const params = parseQueryStringIntoObject(window.location.search.substr(1)); - const url = mergeUrlParams(params, `${wikisPath}/${slug}`); - redirectTo(url); + // Replace hyphens with spaces + if (title) title = title.replace(/-+/g, ' '); - e.preventDefault(); - } + const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title }); + this.commitMessageInput.value = newCommitMessage; } handleToggleSidebar(e) { diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index e01080b04d6..4efc1d2408a 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -104,7 +104,7 @@ export default { v-gl-tooltip :title=" __( - 'The code of a detached pipeline is tested against the source branch instead of merged results', + 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.', ) " class="js-pipeline-url-detached badge badge-info" diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index cdf2d1020ba..beb2ac09992 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -26,11 +26,6 @@ export default { required: false, default: false, }, - forceModifiedIcon: { - type: Boolean, - required: false, - default: false, - }, size: { type: Number, required: false, @@ -48,8 +43,6 @@ export default { // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; - if (this.forceModifiedIcon) return `file-modified${suffix}`; - return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { @@ -88,7 +81,7 @@ export default { v-gl-tooltip.right :title="tooltipTitle" :class="{ 'ml-auto': isCentered }" - class="file-changed-icon" + class="file-changed-icon d-inline-block" > <icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index fe5289ff371..f49e69c473b 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -146,6 +146,7 @@ export default { <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> <file-icon v-if="!showChangedIcon || file.type === 'tree'" + class="file-row-icon" :file-name="file.name" :loading="file.loading" :folder="isTree" @@ -223,13 +224,8 @@ export default { white-space: nowrap; } -.file-row-name svg { +.file-row-name .file-row-icon { margin-right: 2px; vertical-align: middle; } - -.file-row-name .loading-container { - display: inline-block; - margin-right: 4px; -} </style> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index df19906309c..f0aae20477b 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -30,9 +30,16 @@ export default { }, mounted() { + if (window.recaptchaDialogCallback) { + throw new Error('recaptchaDialogCallback is already defined!'); + } window.recaptchaDialogCallback = this.submit.bind(this); }, + beforeDestroy() { + window.recaptchaDialogCallback = null; + }, + methods: { appendRecaptchaScript() { this.removeRecaptchaScript(); diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js new file mode 100644 index 00000000000..4659ec20ceb --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js @@ -0,0 +1,39 @@ +/** + * Input/Textarea Autofocus Directive for Vue + */ +export default { + /** + * Set focus when element is rendered, but + * is not visible, using IntersectionObserver + * + * @param {Element} el Target element + */ + inserted(el) { + if ('IntersectionObserver' in window) { + // Element visibility is dynamic, so we attach observer + el.visibilityObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + // Combining `intersectionRatio > 0` and + // element's `offsetParent` presence will + // deteremine if element is truely visible + if (entry.intersectionRatio > 0 && entry.target.offsetParent) { + entry.target.focus(); + } + }); + }); + + // Bind the observer. + el.visibilityObserver.observe(el, { root: document.documentElement }); + } + }, + /** + * Detach observer on unbind hook. + * + * @param {Element} el Target element + */ + unbind(el) { + if (el.visibilityObserver) { + el.visibilityObserver.disconnect(); + } + }, +}; diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss index d287215096e..89029a58d1e 100644 --- a/app/assets/stylesheets/errors.scss +++ b/app/assets/stylesheets/errors.scss @@ -96,7 +96,7 @@ a { } .error-nav { - padding: 0; + padding: $gl-padding 0 0; text-align: center; li { diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index c6060161dec..c036267a7c8 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -1,6 +1,6 @@ .badge.badge-pill { font-weight: $gl-font-weight-normal; background-color: $badge-bg; - color: $gl-text-color-secondary; + color: $gray-800; vertical-align: baseline; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index f384a49e0ae..e9218dcec67 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -438,6 +438,7 @@ img.emoji { .w-3rem { width: 3rem; } .h-12em { height: 12em; } +.h-32-px { height: 32px;} .mw-460 { max-width: 460px; } .mw-6em { max-width: 6em; } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e3dd127366d..96f6d02a68f 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -43,6 +43,7 @@ @extend .alert; background-color: $orange-100; color: $orange-900; + cursor: default; margin: 0; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1bc597bd4ae..ca737c53318 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -131,7 +131,6 @@ > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; - min-width: 100%; } } } @@ -233,7 +232,6 @@ .impersonation-btn, .impersonation-btn:hover { background-color: $white-light; - margin-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index ac3214a07d9..bdeac7e97c0 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -16,3 +16,16 @@ color: $dark-diff-match-bg; background: $dark-diff-match-color; } + +@mixin diff-expansion($background, $border, $link) { + background-color: $background; + + td { + border-top: 1px solid $border; + border-bottom: 1px solid $border; + } + + a { + color: $link; + } +} diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index 16893dd047e..cbce0ba3f1e 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -111,6 +111,10 @@ $dark-il: #de935f; color: $dark-line-color; } + .line_expansion { + @include diff-expansion($dark-main-bg, $dark-border, $dark-na); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 37fe61b925c..1b61ffa37e3 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -111,6 +111,10 @@ $monokai-gi: #a6e22e; color: $monokai-text-color; } + .line_expansion { + @include diff-expansion($monokai-bg, $monokai-border, $monokai-k); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index b4217aac37a..a7ede266fb5 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -34,8 +34,11 @@ color: $gl-text-color; } -// Diff line + .line_expansion { + @include diff-expansion($gray-light, $white-normal, $gl-text-color); + } + // Diff line $none-over-bg: #ded7fc; $none-expanded-border: #e0e0e0; $none-expanded-bg: #e0e0e0; diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index a4e9eda22c9..6569f3abc8b 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -115,6 +115,10 @@ $solarized-dark-il: #2aa198; color: $solarized-dark-pre-color; } + .line_expansion { + @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index b604d1ccb6c..4e74a9ea50a 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -122,6 +122,10 @@ $solarized-light-il: #2aa198; color: $solarized-light-pre-color; } + .line_expansion { + @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index b3974df8639..973f94c63aa 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -101,24 +101,8 @@ pre.code, color: $white-code-color; } -// Expansion line .line_expansion { - background-color: $gray-light; - - td { - border-top: 1px solid $border-color; - border-bottom: 1px solid $border-color; - text-align: center; - } - - a { - color: $blue-600; - } - - .unfold-icon { - display: inline-block; - padding: 8px 0; - } + @include diff-expansion($gray-light, $border-color, $blue-600); } // Diff line diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 2b932d164a5..d80155a416d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -51,27 +51,19 @@ } .stage-header { - width: 26%; - padding-left: $gl-padding; + width: 18.5%; } .median-header { - width: 14%; + width: 21.5%; } .event-header { width: 45%; - padding-left: $gl-padding; } .total-time-header { width: 15%; - text-align: right; - padding-right: $gl-padding; - } - - .stage-name { - font-weight: $gl-font-weight-bold; } } @@ -153,23 +145,13 @@ } .stage-nav-item { - display: flex; line-height: 65px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - border-right: 1px solid $border-color; - background-color: $gray-light; + border: 1px solid $border-color; &.active { - background-color: transparent; - border-right-color: transparent; - border-top-color: $border-color; - border-bottom-color: $border-color; - box-shadow: inset 2px 0 0 0 $blue-500; - - .stage-name { - font-weight: $gl-font-weight-bold; - } + background: $blue-50; + border-color: $blue-300; + box-shadow: inset 4px 0 0 0 $blue-500; } &:hover:not(.active) { @@ -178,24 +160,12 @@ cursor: pointer; } - &:first-child { - border-top: 0; - } - - &:last-child { - border-bottom: 0; - } - - .stage-nav-item-cell { - &.stage-median { - margin-left: auto; - margin-right: $gl-padding; - min-width: calc(35% - #{$gl-padding}); - } + .stage-nav-item-cell.stage-name { + width: 44.5%; } - .stage-name { - padding-left: 16px; + .stage-nav-item-cell.stage-median { + min-width: 43%; } .stage-empty, diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index ffb27e54f34..77a2fd6b876 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1032,7 +1032,6 @@ table.code { $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; max-height: calc(100vh - #{$top-pos}); - padding-right: $gl-padding; z-index: 202; .with-performance-bar & { @@ -1043,7 +1042,7 @@ table.code { .drag-handle { bottom: 16px; - transform: translateX(-6px); + transform: translateX(10px); } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 66b4f3bad2b..fa52ce6402d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -522,6 +522,7 @@ } .multiple-users { + position: relative; height: 24px; margin-bottom: 17px; margin-top: 4px; diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index 85e9f303dde..0fbf7033aa5 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -48,11 +48,6 @@ padding: $gl-padding-top $gl-padding; border-top: 1px solid $border-color; } - - .report-block-list-icon .loading-container { - position: relative; - left: -2px; - } } .report-block-container { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5e65084a110..af6644b8fcc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar include SessionlessAuthentication + include ConfirmEmailWarning before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb new file mode 100644 index 00000000000..5a4b5897a4f --- /dev/null +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ConfirmEmailWarning + extend ActiveSupport::Concern + + included do + before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } + end + + protected + + def set_confirm_warning + return unless current_user + return if current_user.confirmed? + return if peek_request? || json_request? || !request.get? + + email = current_user.unconfirmed_email || current_user.email + + flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % { + email: email, + resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post), + update_link: view_context.link_to(_('Update it'), profile_path) + } + end +end diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb index c9f66e5c194..45c0a5c58ef 100644 --- a/app/controllers/concerns/invisible_captcha.rb +++ b/app/controllers/concerns/invisible_captcha.rb @@ -41,9 +41,9 @@ module InvisibleCaptcha request_information = { message: message, env: :invisible_captcha_signup_bot_detected, - ip: request.ip, + remote_ip: request.ip, request_method: request.request_method, - fullpath: request.fullpath + path: request.fullpath } Gitlab::AuthLogger.error(request_information) diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 2ae500a2fdf..b192189ba3c 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -11,7 +11,7 @@ class ConfirmationsController < Devise::ConfirmationsController protected def after_resending_confirmation_instructions_path_for(resource) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? stored_location_for(resource) || dashboard_projects_path : users_almost_there_path end def after_confirmation_path_for(resource_name, resource) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index b04ffe80db4..4125f44d00a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController def diff apply_diff_view_cookie! - @form = Blobs::UnfoldPresenter.new(blob, params.to_unsafe_h) + @form = Blobs::UnfoldPresenter.new(blob, diff_params) # keep only json rendering when # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done @@ -239,4 +239,8 @@ class Projects::BlobController < Projects::ApplicationController def tree_path @path.rpartition('/').first end + + def diff_params + params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent) + end end diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb index c8facea1d70..4efe956e973 100644 --- a/app/controllers/projects/starrers_controller.rb +++ b/app/controllers/projects/starrers_controller.rb @@ -5,25 +5,11 @@ class Projects::StarrersController < Projects::ApplicationController def index @starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute - - # Normally the number of public starrers is equal to the number of visible - # starrers. We need to fix the counts in two cases: when the current user - # is an admin (and can see everything) and when the current user has a - # private profile and has starred the project (and can see itself). - @public_count = - if @current_user&.admin? - @starrers.with_public_profile.count - elsif @current_user&.private_profile && has_starred_project?(@starrers) - @starrers.size - 1 - else - @starrers.size - end - - @total_count = @project.starrers.size - @private_count = @total_count - @public_count - @sort = params[:sort].presence || sort_value_name - @starrers = @starrers.sort_by_attribute(@sort).page(params[:page]) + @starrers = @starrers.preload_users.sort_by_attribute(@sort).page(params[:page]) + @public_count = @project.starrers.with_public_profile.size + @total_count = @project.starrers.size + @private_count = @total_count - @public_count end private diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index d1914c35bd3..b187fdb2723 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -16,6 +16,10 @@ class Projects::WikisController < Projects::ApplicationController redirect_to(project_wiki_path(@project, @page)) end + def new + redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true) + end + def pages @wiki_pages = Kaminari.paginate_array( @project_wiki.list_pages(sort: params[:sort], direction: params[:direction]) @@ -24,17 +28,25 @@ class Projects::WikisController < Projects::ApplicationController @wiki_entries = WikiPage.group_by_directory(@wiki_pages) end + # `#show` handles a number of scenarios: + # + # - If `id` matches a WikiPage, then show the wiki page. + # - If `id` is a file in the wiki repository, then send the file. + # - If we know the user wants to create a new page with the given `id`, + # then display a create form. + # - Otherwise show the empty wiki page and invite the user to create a page. def show - view_param = @project_wiki.empty? ? params[:view] : 'create' - if @page set_encoding_error unless valid_encoding? render 'show' elsif file_blob send_blob(@project_wiki.repository, file_blob) - elsif can?(current_user, :create_wiki, @project) && view_param == 'create' - @page = build_page(title: params[:id]) + elsif show_create_form? + # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new + title = params[:id] unless params[:random_title].present? + + @page = build_page(title: title) render 'edit' else @@ -110,6 +122,15 @@ class Projects::WikisController < Projects::ApplicationController private + def show_create_form? + can?(current_user, :create_wiki, @project) && + @page.nil? && + # Always show the create form when the wiki has had at least one page created. + # Otherwise, we only show the form when the user has navigated from + # the 'empty wiki' page + (@project_wiki.exists? || params[:view] == 'create') + end + def load_project_wiki @project_wiki = load_wiki @@ -135,7 +156,7 @@ class Projects::WikisController < Projects::ApplicationController params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) end - def build_page(args) + def build_page(args = {}) WikiPage.new(@project_wiki).tap do |page| page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index db10515c0b4..e773ec09924 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -69,12 +69,12 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?)) - user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path + confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path end def after_inactive_sign_up_path_for(resource) Gitlab::AppLogger.info(user_created_message) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? dashboard_projects_path : users_almost_there_path end private @@ -135,4 +135,12 @@ class RegistrationsController < Devise::RegistrationsController def terms_accepted? Gitlab::Utils.to_boolean(params[:terms_opt_in]) end + + def confirmed_or_unconfirmed_access_allowed(user) + user.confirmed? || Feature.enabled?(:soft_email_confirmation) + end + + def stored_location_or_dashboard(user) + stored_location_for(user) || dashboard_projects_path + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 160f9ac4793..bd26bd01313 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,6 +31,11 @@ module GroupsHelper can?(current_user, :change_share_with_group_lock, group) end + def can_disable_group_emails?(group) + Feature.enabled?(:emails_disabled, group, default_enabled: true) && + can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled? + end + def group_issues_count(state:) IssuesFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 11b9cf22142..5678304ffcf 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -5,7 +5,7 @@ module NotificationsHelper def notification_icon_class(level) case level.to_sym - when :disabled + when :disabled, :owner_disabled 'microphone-slash' when :participating 'volume-up' @@ -18,6 +18,16 @@ module NotificationsHelper end end + def notification_icon_level(notification_setting, emails_disabled = false) + if emails_disabled + 'owner_disabled' + elsif notification_setting.global? + current_user.global_notification_setting.level + else + notification_setting.level + end + end + def notification_icon(level, text = nil) icon("#{notification_icon_class(level)} fw", text: text) end @@ -53,6 +63,8 @@ module NotificationsHelper _('Use your global notification setting') when :custom _('You will only receive notifications for the events you choose') + when :owner_disabled + _('Notifications have been disabled by the project or group owner') end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 71c9c121e48..33bf2d57fae 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -155,6 +155,12 @@ module ProjectsHelper end end + def can_disable_emails?(project, current_user) + return false if project.group&.emails_disabled? + + can?(current_user, :set_emails_disabled, project) && Feature.enabled?(:emails_disabled, project, default_enabled: true) + end + def last_push_event current_user&.recent_push(@project) end @@ -541,13 +547,15 @@ module ProjectsHelper snippetsAccessLevel: feature.snippets_access_level, pagesAccessLevel: feature.pages_access_level, containerRegistryEnabled: !!project.container_registry_enabled, - lfsEnabled: !!project.lfs_enabled + lfsEnabled: !!project.lfs_enabled, + emailsDisabled: project.emails_disabled? } end def project_permissions_panel_data(project) { currentSettings: project_permissions_settings(project), + canDisableEmails: can_disable_emails?(project, current_user), canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), allowedVisibilityOptions: project_allowed_visibility_levels(project), visibilityHelpPath: help_page_path('public_access/public_access'), diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index f705e67121f..3c0efca31db 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -716,7 +716,7 @@ module Ci depended_jobs = depends_on_builds # find all jobs that are needed - if Feature.enabled?(:ci_dag_support, project) && needs.exists? + if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists? depended_jobs = depended_jobs.where(name: needs.select(:name)) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3b28eb246db..0a943a33bbb 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -328,6 +328,10 @@ module Ci config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) end + def self.bridgeable_statuses + ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending] + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb index dab034b7234..5556fc8d3f0 100644 --- a/app/models/clusters/clusters_hierarchy.rb +++ b/app/models/clusters/clusters_hierarchy.rb @@ -46,7 +46,7 @@ module Clusters def group_clusters_base_query group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id') - join_sources = ::Group.left_joins(:clusters).join_sources + join_sources = ::Group.left_joins(:clusters).arel.join_sources model .unscoped @@ -59,7 +59,7 @@ module Clusters def project_clusters_base_query projects = ::Project.arel_table project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id') - join_sources = ::Project.left_joins(:clusters).join_sources + join_sources = ::Project.left_joins(:clusters).arel.join_sources model .unscoped diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index f6b19317c50..3d6f397e599 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -15,8 +15,8 @@ class GroupMember < Member default_scope { where(source_type: SOURCE_TYPE) } scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } - scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } + scope :of_ldap_type, -> { where(ldap: true) } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c91add6439f..4a19e05bf76 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -85,6 +85,10 @@ class ProjectWiki list_pages(limit: 1).empty? end + def exists? + !empty? + end + # Lists wiki pages of the repository. # # limit - max number of pages returned by the method. diff --git a/app/models/repository.rb b/app/models/repository.rb index 9d45a12fa6e..b957b9b0bdd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -239,13 +239,13 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - branch_names.include?(branch_name) + branch_names_include?(branch_name) end def tag_exists?(tag_name) return false unless raw_repository - tag_names.include?(tag_name) + tag_names_include?(tag_name) end def ref_exists?(ref) @@ -389,11 +389,15 @@ class Repository expire_statistics_caches end - # Runs code after a repository has been created. - def after_create + def expire_status_cache expire_exists_cache expire_root_ref_cache expire_emptiness_caches + end + + # Runs code after a repository has been created. + def after_create + expire_status_cache repository_event(:create_repository) end @@ -561,10 +565,10 @@ class Repository end delegate :branch_names, to: :raw_repository - cache_method :branch_names, fallback: [] + cache_method_as_redis_set :branch_names, fallback: [] delegate :tag_names, to: :raw_repository - cache_method :tag_names, fallback: [] + cache_method_as_redis_set :tag_names, fallback: [] delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository cache_method :branch_count, fallback: 0 @@ -1126,6 +1130,10 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end + def redis_set_cache + @redis_set_cache ||= Gitlab::RepositorySetCache.new(self) + end + def request_store_cache @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end diff --git a/app/models/user.rb b/app/models/user.rb index 374e00987c5..6131a8dc710 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1507,6 +1507,13 @@ class User < ApplicationRecord super end + # override from Devise::Confirmable + def confirmation_period_valid? + return false if Feature.disabled?(:soft_email_confirmation) + + super + end + private def default_private_profile_to_false diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 3c7a805cc5c..c633e2d8b3d 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -17,6 +17,7 @@ class UsersStarProject < ApplicationRecord scope :by_project, -> (project) { where(project_id: project.id) } scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) } scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) } + scope :preload_users, -> { preload(:user) } class << self def sort_by_attribute(method) diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb index 4c6dd6895cf..a256dd05a4d 100644 --- a/app/presenters/blobs/unfold_presenter.rb +++ b/app/presenters/blobs/unfold_presenter.rb @@ -1,30 +1,34 @@ # frozen_string_literal: true -require 'gt_one_coercion' - module Blobs class UnfoldPresenter < BlobPresenter - include Virtus.model + include ActiveModel::Attributes + include ActiveModel::AttributeAssignment include Gitlab::Utils::StrongMemoize - attribute :full, Boolean, default: false - attribute :since, GtOneCoercion - attribute :to, Integer - attribute :bottom, Boolean - attribute :unfold, Boolean, default: true - attribute :offset, Integer - attribute :indent, Integer, default: 0 + attribute :full, :boolean, default: false + attribute :since, :integer, default: 1 + attribute :to, :integer, default: 1 + attribute :bottom, :boolean, default: false + attribute :unfold, :boolean, default: true + attribute :offset, :integer, default: 0 + attribute :indent, :integer, default: 0 + + alias_method :full?, :full + alias_method :bottom?, :bottom + alias_method :unfold?, :unfold def initialize(blob, params) + super(blob) + self.attributes = params + # Load all blob data first as we need to ensure they're all loaded first # so we can accurately show the rest of the diff when unfolding. load_all_blob_data - @subject = blob @all_lines = blob.data.lines - super(params) - self.attributes = prepare_attributes + handle_full_or_end! end # Returns an array of Gitlab::Diff::Line with match line added @@ -56,21 +60,18 @@ module Blobs private - def prepare_attributes - return attributes unless full? || to == -1 + def handle_full_or_end! + return unless full? || to == -1 - full_opts = { - since: 1, + self.since = 1 if full? + + self.attributes = { to: all_lines_size, bottom: false, unfold: false, offset: 0, indent: 0 } - - return full_opts if full? - - full_opts.merge(attributes.slice(:since)) end def all_lines_size diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 943c707218d..6e91317eb20 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -18,6 +18,7 @@ class DeploymentEntity < Grape::Entity end expose :created_at + expose :finished_at expose :tag expose :last? expose :user, using: UserEntity diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb index 04db6b88489..3fd3e1b9cc8 100644 --- a/app/serializers/deployment_serializer.rb +++ b/app/serializers/deployment_serializer.rb @@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer entity DeploymentEntity def represent_concise(resource, opts = {}) - opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]] + opts[:only] = [:iid, :id, :sha, :created_at, :finished_at, :tag, :last?, :id, ref: [:name]] represent(resource, opts) end end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 61de3c93337..c02fd024345 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -98,6 +98,10 @@ class IssuableSidebarBasicEntity < Grape::Entity autocomplete_projects_path(project_id: issuable.project.id) end + expose :project_emails_disabled do |issuable| + issuable.project.emails_disabled? + end + private def current_user diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index f4bd457ebc6..3b145a65d79 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -40,7 +40,7 @@ module Ci def process_builds_with_needs(trigger_build_ids) return false unless trigger_build_ids.present? - return false unless Feature.enabled?(:ci_dag_support, project) + return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) # we find processables that are dependent: # 1. because of current dependency, @@ -96,7 +96,7 @@ module Ci end def created_processables_without_needs - if Feature.enabled?(:ci_dag_support, project) + if Feature.enabled?(:ci_dag_support, project, default_enabled: true) pipeline.processables.created.without_needs else pipeline.processables.created diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index 1db18fcf401..47c308c8280 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -8,8 +8,6 @@ module Git PROCESS_COMMIT_LIMIT = 100 def execute - project.repository.after_create if project.empty_repo? - create_events create_pipelines execute_project_hooks @@ -58,7 +56,7 @@ module Git return unless params.fetch(:create_pipelines, true) Ci::CreatePipelineService - .new(project, current_user, base_params) + .new(project, current_user, pipeline_params) .execute(:push, pipeline_options) end @@ -70,31 +68,36 @@ module Git end def enqueue_invalidate_cache - ProjectCacheWorker.perform_async( - project.id, - invalidated_file_types, - [:commit_count, :repository_size] - ) + file_types = invalidated_file_types + + return unless file_types.present? + + ProjectCacheWorker.perform_async(project.id, file_types, [], false) end - def base_params + def pipeline_params { - oldrev: params[:oldrev], - newrev: params[:newrev], + before: params[:oldrev], + after: params[:newrev], ref: params[:ref], - push_options: params[:push_options] || {} + push_options: params[:push_options] || {}, + checkout_sha: Gitlab::DataBuilder::Push.checkout_sha( + project.repository, params[:newrev], params[:ref]) } end def push_data_params(commits:, with_changed_files: true) - base_params.merge( + { + oldrev: params[:oldrev], + newrev: params[:newrev], + ref: params[:ref], project: project, user: current_user, commits: commits, message: event_message, commits_count: commits_count, with_changed_files: with_changed_files - ) + } end def event_push_data diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 431a5aedf2e..d2b037a680c 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -83,8 +83,16 @@ module Git # Schedules processing of commit messages def enqueue_process_commit_messages - limited_commits.each do |commit| - next unless commit.matches_cross_reference_regex? + referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) + + upstream_commit_ids = upstream_commit_ids(referencing_commits) + + referencing_commits.each do |commit| + # Avoid reprocessing commits that already exist upstream if the project + # is a fork. This will prevent duplicated/superfluous system notes on + # mentionables referenced by a commit that is pushed to the upstream, + # that is then also pushed to forks when these get synced by users. + next if upstream_commit_ids.include?(commit.id) ProcessCommitWorker.perform_async( project.id, @@ -142,5 +150,18 @@ module Git def branch_name strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) } end + + def upstream_commit_ids(commits) + set = Set.new + + upstream_project = project.fork_source + if upstream_project + upstream_project + .commits_by(oids: commits.map(&:id)) + .each { |commit| set << commit.id } + end + + set + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e30debbbe75..ee7223d6349 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -598,11 +598,11 @@ module SystemNoteService end def zoom_link_added(issue, project, author) - create_note(NoteSummary.new(issue, project, author, _('a Zoom call was added to this issue'), action: 'pinned_embed')) + create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed')) end def zoom_link_removed(issue, project, author) - create_note(NoteSummary.new(issue, project, author, _('a Zoom call was removed from this issue'), action: 'pinned_embed')) + create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed')) end private diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml index 82781f6716d..86f09bf1cb0 100644 --- a/app/views/admin/applications/_delete_form.html.haml +++ b/app/views/admin/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag admin_application_path(application) do %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css + = submit_tag 'Destroy', class: submit_btn_css, data: { confirm: _('Are you sure?') } diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 69cc510e9c1..9bc5e2ee42f 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -5,4 +5,4 @@ = form_tag path do %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - = submit_tag _('Revoke'), onclick: "return confirm('#{_('Are you sure?')}')", class: 'btn btn-remove btn-sm' + = submit_tag _('Revoke'), class: 'btn btn-remove btn-sm', data: { confirm: _('Are you sure?') } diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 46931b5932d..1ed7b56db1d 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -10,7 +10,7 @@ = message %p = s_('403|Please contact your GitLab administrator to get permission.') - .action-container.js-go-back{ style: 'display: none' } - %a{ href: 'javascript:history.back()', class: 'btn btn-success' } + .action-container.js-go-back{ hidden: true } + %button{ type: 'button', class: 'btn btn-success' } = s_('Go Back') = render "errors/footer" diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 4daf3683eaf..e50d2b8e994 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,4 +1,5 @@ - can_create_subgroups = can?(current_user, :create_subgroup, @group) +- emails_disabled = @group.emails_disabled? .group-home-panel .row.mb-3 @@ -21,7 +22,7 @@ .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .group-buttons - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled - if can? current_user, :create_projects, @group - new_project_label = _("New project") - new_subgroup_label = _("New subgroup") diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index d3375e00bad..94a938021f9 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -11,13 +11,20 @@ .form-check = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' = f.label :share_with_group_lock, class: 'form-check-label' do - %span + %span.d-block - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %br %span.descr.text-muted= share_with_group_lock_help_text(@group) + .form-group.append-bottom-default + .form-check + = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' + = f.label :emails_disabled, class: 'form-check-label' do + %span.d-block= s_('GroupSettings|Disable email notifications') + %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') + = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group + = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 74484005b48..dc924a0e25d 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -14,6 +14,10 @@ var goBackElement = document.querySelector('.js-go-back'); if (goBackElement && history.length > 1) { - goBackElement.style.display = 'block'; + goBackElement.removeAttribute('hidden'); + + goBackElement.querySelector('button').addEventListener('click', function() { + history.back(); + }); } }()); diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 89f99472270..14c7b2428b2 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -4,6 +4,7 @@ - search_path_url = search_path(group_id: group.id) - else - search_path_url = search_path +- has_impersonation_link = header_link?(:admin_impersonation) %header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } } %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content @@ -64,14 +65,14 @@ .dropdown-menu.dropdown-menu-right = render 'layouts/header/help_dropdown' - if header_link?(:user_dropdown) - %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } } + %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' - - if header_link?(:admin_impersonation) - %li.nav-item.impersonation + - if has_impersonation_link + %li.nav-item.impersonation.ml-0 = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret') - if header_link?(:sign_in) diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index cf17ee44145..1776d260e19 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -1,16 +1,15 @@ +- emails_disabled = group.emails_disabled? + .gl-responsive-table-row.notification-list-item .table-section.section-40 %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to group.name, group_path(group) .table-section.section-30.text-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled .table-section.section-30 = form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f| diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index 823fec3fab4..63a77b335b6 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -1,12 +1,11 @@ +- emails_disabled = project.emails_disabled? + %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to_project(project) .float-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 65ef9690062..08a39fc4f58 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -31,7 +31,7 @@ = s_('AccessTokens|It cannot be used to access any other data.') .col-lg-8.feed-token-reset = label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold" - = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()' + = text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true %p.form-text.text-muted - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') } - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link } @@ -49,7 +49,7 @@ = s_('AccessTokens|It cannot be used to access any other data.') .col-lg-8.incoming-email-token-reset = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold" - = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()' + = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true %p.form-text.text-muted - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') } - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 824fe3c791d..4783b10cf6d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,8 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 +- emails_disabled = @project.emails_disabled? + .project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] } .row.append-bottom-8 .home-panel-title-row.col-md-12.col-lg-6.d-flex @@ -41,7 +43,7 @@ .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .d-inline-flex - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled .count-buttons.d-inline-flex = render 'projects/buttons/star' diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index ae9aef5a9b0..e1bf0940f59 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } } + %button{ tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 1331fa179fc..cbd998c60ef 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } +%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 59f0afd59e6..2b594c125f4 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -34,40 +34,29 @@ {{ n__('Last %d day', 'Last %d days', 90) }} .stage-panel-container .card.stage-panel - .card-header + .card-header.border-bottom-0 %nav.col-headers %ul - %li.stage-header - %span.stage-name + %li.stage-header.pl-5 + %span.stage-name.font-weight-bold {{ s__('ProjectLifecycle|Stage') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" } %li.median-header - %span.stage-name + %span.stage-name.font-weight-bold {{ __('Median') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" } - %li.event-header - %span.stage-name + %li.event-header.pl-3 + %span.stage-name.font-weight-bold {{ currentStage ? __(currentStage.legend) : __('Related Issues') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" } - %li.total-time-header - %span.stage-name + %li.total-time-header.pr-5.text-right + %span.stage-name.font-weight-bold {{ __('Total Time') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" } .stage-panel-body %nav.stage-nav %ul - %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" } - .stage-nav-item-cell.stage-name - {{ stage.title }} - .stage-nav-item-cell.stage-median - %template{ "v-if" => "stage.isUserAllowed" } - %span{ "v-if" => "stage.value" } - {{ stage.value }} - %span.stage-empty{ "v-else" => true } - {{ __('Not enough data') }} - %template{ "v-else" => true } - %span.not-available - {{ __('Not available') }} + %stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" } .section.stage-events %template{ "v-if" => "isLoadingStage" } = icon("spinner spin") diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml new file mode 100644 index 00000000000..42631fca5e8 --- /dev/null +++ b/app/views/projects/pages_domains/_certificate.html.haml @@ -0,0 +1,18 @@ +- if @domain.auto_ssl_enabled? + - if @domain.enabled? + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .bs-callout.bs-callout-info + = _("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.") + - else + .bs-callout.bs-callout-warning + = _("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.") +- else + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .light + = _("missing") diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index e9019175219..d0b54946f7e 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -60,9 +60,4 @@ %td = _("Certificate") %td - - if @domain.certificate_text - %pre - = @domain.certificate_text - - else - .light - = _("missing") + = render 'certificate' diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 2b56ada8b73..9614f33fe2f 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -43,7 +43,7 @@ } } Auto DevOps - if @pipeline.detached_merge_request_pipeline? - %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "The code of a detached pipeline is tested against the source branch instead of merged results" } + %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.') } detached - if @pipeline.stuck? %span.js-pipeline-url-stuck.badge.badge-warning diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 858731b2dda..a153f527ee0 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,8 +1,8 @@ -- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") -- commit_message = commit_message % { page_title: @page.title } +- form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit' +- form_classes += ' js-new-wiki-page' unless @page.persisted? = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, - html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' }, + html: { class: form_classes }, data: { uploads_path: uploads_path } do |f| = form_errors(@page) @@ -12,12 +12,14 @@ .form-group.row .col-sm-12= f.label :title, class: 'control-label-full-width' .col-sm-12 - = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title - - if @page.persisted? - %span.d-inline-block.mw-100.prepend-top-5 - = icon('lightbulb-o') + = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title') + %span.d-inline-block.mw-100.prepend-top-5 + = icon('lightbulb-o') + - if @page.persisted? = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' + - else + = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-group.row .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 @@ -43,7 +45,7 @@ .form-group.row .col-sm-12= f.label :commit_message, class: 'control-label-full-width' - .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message + .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil .form-actions - if @page && @page.persisted? diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 643b51e01d1..2e1e176c42a 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,9 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do + = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do = s_("Wiki|New page") - = link_to project_wiki_history_path(@project, @page), class: "btn" do + = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do = s_("Wiki|Page history") - if can?(current_user, :create_wiki, @project) && @page.latest? && @valid_encoding - = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do + = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit", role: "button" do = _("Edit") diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml deleted file mode 100644 index 2c675c0de9c..00000000000 --- a/app/views/projects/wikis/_new.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -#modal-new-wiki.modal - .modal-dialog - .modal-content - .modal-header - %h3.page-title= s_("WikiNewPageTitle|New Wiki Page") - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } - %span{ "aria-hidden": true } × - .modal-body - %form.new-wiki-page - .form-group - = label_tag :new_wiki_path do - %span= s_("WikiPage|Page slug") - = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true - %span.d-inline-block.mw-100.prepend-top-5 - = icon('lightbulb-o') - = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") - .form-actions - = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success" diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 28353927135..a9d21470944 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -19,5 +19,3 @@ .block = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do = s_("Wiki|More Pages") - -= render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index e8b59a3b8c4..815b4a51261 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -13,14 +13,11 @@ %h2.wiki-page-title - if @page.persisted? = link_to @page.human_title, project_wiki_path(@project, @page) - - else - = @page.human_title - %span.light - · - - if @page.persisted? + %span.light + · = s_("Wiki|Edit Page") - - else - = s_("Wiki|Create Page") + - else + = s_("Wiki|Create New Page") .nav-controls - if @page.persisted? diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b4f8377c008..825088a58e7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -137,7 +137,11 @@ .js-sidebar-participants-entry-point - if signed_in - .js-sidebar-subscriptions-entry-point + - if issuable_sidebar[:project_emails_disabled] + .block.js-emails-disabled + = notification_description(:owner_disabled) + - else + .js-sidebar-subscriptions-entry-point - project_ref = issuable_sidebar[:reference] .block.project-reference diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index e83ca5eaab8..42a823e3a8d 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -32,7 +32,7 @@ %ul - Gitlab::Access.options.each do |role, role_id| %li - = link_to role, "javascript:void(0)", + = link_to role, '#', class: ("is-active" if group_link.group_access == role_id), data: { id: role_id, el_id: dom_id } .clearable-input.member-form-control.d-sm-inline-block diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 331283f7eec..6762f211a80 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -82,7 +82,7 @@ %ul - member.valid_level_roles.each do |role, role_id| %li - = link_to role, "javascript:void(0)", + = link_to role, '#', class: ("is-active" if member.access_level == role_id), data: { id: role_id, el_id: dom_id(member) } = render_if_exists 'shared/members/ee/revert_ldap_group_sync_option', diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 749aa258af6..b4266937a4e 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,6 +1,15 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - aria_label = button_title + - btn_class << " disabled" + - else + - button_title = _("Notification setting") + - aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = hidden_setting_source_input(notification_setting) @@ -8,14 +17,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } .float-left = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml index 052e6da5bae..3c8cc023848 100644 --- a/app/views/shared/notifications/_new_button.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -1,6 +1,13 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - btn_class << " disabled" + - else + - button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f| = hidden_setting_source_input(notification_setting) @@ -9,14 +16,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" } = sprite_icon("arrow-down", css_class: "icon mr-0") .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden = sprite_icon("arrow-down", css_class: "icon") diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 622bd6f1f48..61d34981458 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -42,10 +42,8 @@ class PostReceive user = identify_user(post_received) return false unless user - # Expire the branches cache so we have updated data for this push - post_received.project.repository.expire_branches_cache if post_received.includes_branches? - # We only need to expire tags once per push - post_received.project.repository.expire_caches_for_tags if post_received.includes_tags? + # We only need to expire certain caches once per push + expire_caches(post_received) post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| service_klass = @@ -74,6 +72,30 @@ class PostReceive after_project_changes_hooks(post_received, user, refs.to_a, changes) end + # Expire the project, branch, and tag cache once per push. Schedule an + # update for the repository size and commit count if necessary. + def expire_caches(post_received) + project = post_received.project + + project.repository.expire_status_cache if project.empty_repo? + project.repository.expire_branches_cache if post_received.includes_branches? + project.repository.expire_caches_for_tags if post_received.includes_tags? + + enqueue_repository_cache_update(post_received) + end + + def enqueue_repository_cache_update(post_received) + stats_to_invalidate = [:repository_size] + stats_to_invalidate << :commit_count if post_received.includes_default_branch? + + ProjectCacheWorker.perform_async( + post_received.project.id, + [], + stats_to_invalidate, + true + ) + end + def after_project_changes_hooks(post_received, user, refs, changes) hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 3efb5343a96..f6ebe4ab006 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -2,7 +2,8 @@ # Worker for processing individual commit messages pushed to a repository. # -# Jobs for this worker are scheduled for every commit that is being pushed. As a +# Jobs for this worker are scheduled for every commit that contains mentionable +# references in its message and does not exist in the upstream project. As a # result of this the workload of this worker should be kept to a bare minimum. # Consider using an extra worker if you need to add any extra (and potentially # slow) processing of commits. @@ -19,7 +20,6 @@ class ProcessCommitWorker project = Project.find_by(id: project_id) return unless project - return if commit_exists_in_upstream?(project, commit_hash) user = User.find_by(id: user_id) @@ -77,17 +77,4 @@ class ProcessCommitWorker Commit.from_hash(hash, project) end - - private - - # Avoid reprocessing commits that already exist in the upstream - # when project is forked. This will also prevent duplicated system notes. - def commit_exists_in_upstream?(project, commit_hash) - upstream_project = project.fork_source - - return false unless upstream_project - - commit_id = commit_hash.with_indifferent_access[:id] - upstream_project.commit(commit_id).present? - end end diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 4e8ea903139..5ac860c93e0 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -12,13 +12,15 @@ class ProjectCacheWorker # CHANGELOG. # statistics - An Array containing columns from ProjectStatistics to # refresh, if empty all columns will be refreshed + # refresh_statistics - A boolean that determines whether project statistics should + # be updated. # rubocop: disable CodeReuse/ActiveRecord - def perform(project_id, files = [], statistics = []) + def perform(project_id, files = [], statistics = [], refresh_statistics = true) project = Project.find_by(id: project_id) return unless project - update_statistics(project, statistics) + update_statistics(project, statistics) if refresh_statistics return unless project.repository.exists? |