diff options
Diffstat (limited to 'app')
74 files changed, 919 insertions, 355 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 947d019c725..52d9f2f0322 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,8 +1,5 @@ import $ from 'jquery'; -import { DOMParser } from 'prosemirror-model'; import { getSelectedFragment } from '~/lib/utils/common_utils'; -import schema from './schema'; -import markdownSerializer from './serializer'; export class CopyAsGFM { constructor() { @@ -39,9 +36,13 @@ export class CopyAsGFM { div.appendChild(el.cloneNode(true)); const html = div.innerHTML; - clipboardData.setData('text/plain', el.textContent); - clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); - clipboardData.setData('text/html', html); + CopyAsGFM.nodeToGFM(el) + .then(res => { + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', res); + clipboardData.setData('text/html', html); + }) + .catch(() => {}); } static pasteGFM(e) { @@ -137,11 +138,21 @@ export class CopyAsGFM { } static nodeToGFM(node) { - const wrapEl = document.createElement('div'); - wrapEl.appendChild(node.cloneNode(true)); - const doc = DOMParser.fromSchema(schema).parse(wrapEl); - - return markdownSerializer.serialize(doc); + return Promise.all([ + import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'), + ]) + .then(([prosemirrorModel, schema, markdownSerializer]) => { + const { DOMParser } = prosemirrorModel; + const wrapEl = document.createElement('div'); + wrapEl.appendChild(node.cloneNode(true)); + const doc = DOMParser.fromSchema(schema.default).parse(wrapEl); + + const res = markdownSerializer.default.serialize(doc); + return res; + }) + .catch(() => {}); } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 0eb067d4963..680f2031409 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts { const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); const blockquoteEl = document.createElement('blockquote'); blockquoteEl.appendChild(el); - const text = CopyAsGFM.nodeToGFM(blockquoteEl); - - if (text.trim() === '') { - return false; - } - - // If replyField already has some content, add a newline before our quote - const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; - $replyField - .val((a, current) => `${current}${separator}${text}\n\n`) - .trigger('input') - .trigger('change'); - - // Trigger autosize - const event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - $replyField.get(0).dispatchEvent(event); + CopyAsGFM.nodeToGFM(blockquoteEl) + .then(text => { + if (text.trim() === '') { + return false; + } + + // If replyField already has some content, add a newline before our quote + const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; + $replyField + .val((a, current) => `${current}${separator}${text}\n\n`) + .trigger('input') + .trigger('change'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + $replyField.get(0).dispatchEvent(event); + + // Focus the input field + $replyField.focus(); - // Focus the input field - $replyField.focus(); + return false; + }) + .catch(() => {}); return false; } diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 3ef54752436..0bf2dde8b96 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -6,6 +6,7 @@ import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -14,6 +15,7 @@ export default { GlLink, GlButton, SettingsDropdown, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,8 +37,15 @@ export default { }, }, computed: { - ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']), - ...mapGetters('diffs', ['hasCollapsedFile']), + ...mapGetters('diffs', ['hasCollapsedFile', 'diffFilesLength']), + ...mapState('diffs', [ + 'commit', + 'showTreeList', + 'startVersion', + 'latestVersionPath', + 'addedLines', + 'removedLines', + ]), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, @@ -104,6 +113,11 @@ export default { <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> </div> <div class="inline-parallel-buttons d-none d-md-flex ml-auto"> + <diff-stats + :diff-files-length="diffFilesLength" + :added-lines="addedLines" + :removed-lines="removedLines" + /> <gl-button v-if="commit || startVersion" :href="latestVersionPath" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index b58f704bebb..60586d4a607 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -9,6 +9,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; import EditButton from './edit_button.vue'; +import DiffStats from './diff_stats.vue'; export default { components: { @@ -16,6 +17,7 @@ export default { EditButton, Icon, FileIcon, + DiffStats, }, directives: { GlTooltip: GlTooltipDirective, @@ -202,6 +204,7 @@ export default { v-if="!diffFile.submodule && addMergeRequestButtons" class="file-actions d-none d-sm-block" > + <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <template v-if="diffFile.blob && diffFile.blob.readable_text"> <button :disabled="!diffHasDiscussions(diffFile)" diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue new file mode 100644 index 00000000000..2e5855380af --- /dev/null +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -0,0 +1,52 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import { n__ } from '~/locale'; + +export default { + components: { Icon }, + props: { + addedLines: { + type: Number, + required: true, + }, + removedLines: { + type: Number, + required: true, + }, + diffFilesLength: { + type: Number, + required: false, + default: null, + }, + }, + computed: { + filesText() { + return n__('File', 'Files', this.diffFilesLength); + }, + isCompareVersionsHeader() { + return Boolean(this.diffFilesLength); + }, + }, +}; +</script> + +<template> + <div + class="diff-stats" + :class="{ + 'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader, + 'd-inline-flex': !isCompareVersionsHeader, + }" + > + <div v-if="diffFilesLength !== null" class="diff-stats-group"> + <icon name="doc-code" class="diff-stats-icon text-secondary" /> + <strong>{{ diffFilesLength }} {{ filesText }}</strong> + </div> + <div class="diff-stats-group cgreen"> + <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong> + </div> + <div class="diff-stats-group cred"> + <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index a0f09932593..96ae197d8b8 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -14,8 +14,8 @@ export default { FileRow, }, computed: { - ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), - ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), + ...mapState('diffs', ['tree', 'renderTreeList']), + ...mapGetters('diffs', ['allBlobs']), filteredTreeList() { return this.renderTreeList ? this.tree : this.allBlobs; }, @@ -64,13 +64,6 @@ export default { {{ s__('MergeRequest|No files found') }} </p> </div> - <div v-once class="pt-3 pb-3 text-center"> - {{ n__('%d changed file', '%d changed files', diffFilesLength) }} - <div> - <span class="cgreen"> {{ n__('%d addition', '%d additions', addedLines) }} </span> - <span class="cred"> {{ n__('%d deleted', '%d deletions', removedLines) }} </span> - </div> - </div> </div> </template> diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 6ee33d9fc6d..47f78a5db54 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -11,6 +11,8 @@ const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, + addedLines: null, + removedLines: null, endpoint: '', basePath: '', commit: null, diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index dbfcf8cc921..cb1b1173190 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -64,6 +64,7 @@ class DueDateSelect { this.saveDueDate(true); } }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($dueDateInput.val())); @@ -183,6 +184,7 @@ export default class DueDateSelectors { onSelect(dateText) { $datePicker.val(calendar.toString(dateText)); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate(datePickerVal)); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 4d2533d01f1..9336b71cfd7 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -44,6 +44,7 @@ export default class IssuableForm { parse: dateString => parsePikadayDate(dateString), toString: date => pikadayToString(date), onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)), + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index e664269b199..58f14bac8c8 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; -import { __ } from '~/locale'; +import { s__, sprintf } from '~/locale'; +import createFlash from '~/flash'; import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; @@ -91,9 +92,14 @@ export default { }, taskListUpdateError() { - window.Flash( - __( - 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.', + createFlash( + sprintf( + s__( + 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.', + ), + { + issueType: this.issuableType, + }, ), ); diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js index b41ffb44971..82ee83e4348 100644 --- a/app/assets/javascripts/lib/utils/file_upload.js +++ b/app/assets/javascripts/lib/utils/file_upload.js @@ -1,6 +1,9 @@ export default (buttonSelector, fileSelector) => { const btn = document.querySelector(buttonSelector); const fileInput = document.querySelector(fileSelector); + + if (!btn || !fileInput) return; + const form = btn.closest('form'); btn.addEventListener('click', () => { diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index 0beedcacf33..0dabb28ea66 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -33,6 +33,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d toggleClearInput.call($input); }, + firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($input.val())); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index ac3b47cd218..3b42a154af8 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import { __ } from '~/locale'; +import createFlash from '~/flash'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; @@ -40,6 +41,13 @@ function MergeRequest(opts) { document.querySelector('#task_status').innerText = result.task_status; document.querySelector('#task_status_short').innerText = result.task_status_short; }, + onError: () => { + createFlash( + __( + 'Someone edited this merge request at the same time you did. Please refresh the page to see changes.', + ), + ); + }, }); } } diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index ec0e33a1927..14c02db7bcc 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -189,8 +189,8 @@ export default { <template> <div class="prometheus-graph col-12 col-lg-6"> <div class="prometheus-graph-header"> - <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> - <div class="prometheus-graph-widgets"><slot></slot></div> + <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart ref="areaChart" diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 9c5fd93f7d1..895a57785bc 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -160,7 +160,8 @@ export default { {{ s__('Metrics|Environment') }} <div class="dropdown prepend-left-10"> <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span> {{ currentEnvironmentName }} </span> <icon name="chevron-down" /> + <span>{{ currentEnvironmentName }}</span> + <icon name="chevron-down" /> </button> <div v-if="store.environmentsData.length > 0" @@ -172,9 +173,8 @@ export default { :href="environment.metrics_path" :class="{ 'is-active': environment.name == currentEnvironmentName }" class="dropdown-item" + >{{ environment.name }}</a > - {{ environment.name }} - </a> </li> </ul> </div> diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index 8f98be79640..01001d4f3ff 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,7 +1,5 @@ import ProjectsList from '~/projects_list'; -import Star from '../../../star'; document.addEventListener('DOMContentLoaded', () => { new ProjectsList(); // eslint-disable-line no-new - new Star('.project-row'); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index 0b644780ad4..0d69a689316 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ -import monitoringBundle from '~/monitoring/monitoring_bundle'; +import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle'; document.addEventListener('DOMContentLoaded', monitoringBundle); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 3ccad513c05..26d7fa7371d 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -43,10 +43,26 @@ document.addEventListener('DOMContentLoaded', () => { ], }); + const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { + if (firstDayOfWeek === 0) { + return weekDays; + } + + return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => { + const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length]; + + return { + ...acc, + [reorderedDayName]: weekDays[reorderedDayName], + }; + }, {}); + }; + const hourData = chartData(projectChartData.hour); responsiveChart($('#hour-chart'), hourData); - const dayData = chartData(projectChartData.weekDays); + const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); + const dayData = chartData(weekDays); responsiveChart($('#weekday-chart'), dayData); const monthData = chartData(projectChartData.month); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 8a84ac37dab..afa099d0e0b 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -159,7 +159,7 @@ export default class ActivityCalendar { .append('g') .attr('transform', (group, i) => { _.each(group, (stamp, a) => { - if (a === 0 && stamp.day === 0) { + if (a === 0 && stamp.day === this.firstDayOfWeek) { const month = stamp.date.getMonth(); const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace; const lastMonth = _.last(this.months); @@ -205,6 +205,14 @@ export default class ActivityCalendar { y: 29 + this.dayYPos(5), }, ]; + + if (this.firstDayOfWeek === 1) { + days.push({ + text: 'S', + y: 29 + this.dayYPos(7), + }); + } + this.svg .append('g') .selectAll('text') diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 1c3fd58ca74..39cd891c111 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -234,7 +234,7 @@ export default class UserTabs { data, calendarActivitiesPath, utcOffset, - 0, + gon.first_day_of_week, monthsAgo, ); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue new file mode 100644 index 00000000000..a38f25cce35 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue @@ -0,0 +1,40 @@ +<script> +export default { + props: { + value: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + inputId: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <li> + <div class="commit-message-editor"> + <div class="d-flex flex-wrap align-items-center justify-content-between"> + <label class="col-form-label" :for="inputId"> + <strong>{{ label }}</strong> + </label> + <slot name="header"></slot> + </div> + <textarea + :id="inputId" + :value="value" + class="form-control js-gfm-input append-bottom-default commit-message-edit" + required="required" + rows="7" + @input="$emit('input', $event.target.value)" + ></textarea> + <slot name="checkbox"></slot> + </div> + </li> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue new file mode 100644 index 00000000000..b3c1c0e329d --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -0,0 +1,38 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + props: { + commits: { + type: Array, + required: true, + default: () => [], + }, + }, +}; +</script> + +<template> + <div> + <gl-dropdown + right + no-caret + text="Use an existing commit message" + variant="link" + class="mr-commit-dropdown" + > + <gl-dropdown-item + v-for="commit in commits" + :key="commit.short_id" + class="text-nowrap text-truncate" + @click="$emit('input', commit.message)" + > + <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue new file mode 100644 index 00000000000..a1d3a09cca4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -0,0 +1,91 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import _ from 'underscore'; +import { __, n__, sprintf, s__ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + GlButton, + }, + props: { + isSquashEnabled: { + type: Boolean, + required: true, + }, + commitsCount: { + type: Number, + required: false, + default: 0, + }, + targetBranch: { + type: String, + required: true, + }, + }, + data() { + return { + expanded: false, + }; + }, + computed: { + collapseIcon() { + return this.expanded ? 'chevron-down' : 'chevron-right'; + }, + commitsCountMessage() { + return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount); + }, + modifyLinkMessage() { + return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit'); + }, + ariaLabel() { + return this.expanded ? __('Collapse') : __('Expand'); + }, + message() { + return sprintf( + s__( + 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.', + ), + { + commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`, + mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`, + targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`, + }, + false, + ); + }, + }, + methods: { + toggle() { + this.expanded = !this.expanded; + }, + }, +}; +</script> + +<template> + <div> + <div + class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2" + @click="toggle()" + > + <gl-button + :aria-label="ariaLabel" + variant="blank" + class="commit-edit-toggle mr-2" + @click.stop="toggle()" + > + <icon :name="collapseIcon" :size="16" /> + </gl-button> + <span v-if="expanded">{{ __('Collapse') }}</span> + <span v-else> + <span v-html="message"></span> + <gl-button variant="link" class="modify-message-button"> + {{ modifyLinkMessage }} + </gl-button> + </span> + </div> + <div v-show="expanded"><slot></slot></div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index b8f29649eb5..ce4207864ea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -2,17 +2,24 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import { __ } from '~/locale'; import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; import SquashBeforeMerge from './squash_before_merge.vue'; +import CommitsHeader from './commits_header.vue'; +import CommitEdit from './commit_edit.vue'; +import CommitMessageDropdown from './commit_message_dropdown.vue'; export default { name: 'ReadyToMerge', components: { statusIcon, SquashBeforeMerge, + CommitsHeader, + CommitEdit, + CommitMessageDropdown, }, props: { mr: { type: Object, required: true }, @@ -22,27 +29,20 @@ export default { return { removeSourceBranch: this.mr.shouldRemoveSourceBranch, mergeWhenBuildSucceeds: false, - useCommitMessageWithDescription: false, setToMergeWhenPipelineSucceeds: false, - showCommitMessageEditor: false, isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, squashBeforeMerge: this.mr.squash, successSvg, warningSvg, + squashCommitMessage: this.mr.squashCommitMessage, }; }, computed: { shouldShowMergeWhenPipelineSucceedsText() { return this.mr.isPipelineActive; }, - commitMessageLinkTitle() { - const withDesc = 'Include description in commit message'; - const withoutDesc = "Don't include description in commit message"; - - return this.useCommitMessageWithDescription ? withoutDesc : withDesc; - }, status() { const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -84,9 +84,9 @@ export default { }, mergeButtonText() { if (this.isMergingImmediately) { - return 'Merge in progress'; + return __('Merge in progress'); } else if (this.shouldShowMergeWhenPipelineSucceedsText) { - return 'Merge when pipeline succeeds'; + return __('Merge when pipeline succeeds'); } return 'Merge'; @@ -98,7 +98,7 @@ export default { const { commitMessage } = this; return Boolean( !commitMessage.length || - !this.shouldShowMergeControls() || + !this.shouldShowMergeControls || this.isMakingRequest || this.mr.preventMerge, ); @@ -110,18 +110,14 @@ export default { const { commitsCount, enableSquashBeforeMerge } = this.mr; return enableSquashBeforeMerge && commitsCount > 1; }, - }, - methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; }, - updateCommitMessage() { - const cmwd = this.mr.commitMessageWithDescription; - this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription; - this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage; - }, - toggleCommitMessageEditor() { - this.showCommitMessageEditor = !this.showCommitMessageEditor; + }, + methods: { + updateMergeCommitMessage(includeDescription) { + const { commitMessageWithDescription, commitMessage } = this.mr; + this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; }, handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) { // TODO: Remove no-param-reassign @@ -139,6 +135,7 @@ export default { merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, squash: this.squashBeforeMerge, + squash_commit_message: this.squashCommitMessage, }; this.isMakingRequest = true; @@ -158,7 +155,7 @@ export default { }) .catch(() => { this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line }); }, initiateMergePolling() { @@ -194,7 +191,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while merging this merge request. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line }); }, initiateRemoveSourceBranchPolling() { @@ -223,7 +220,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line + new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line }); }, }, @@ -231,127 +228,136 @@ export default { </script> <template> - <div class="mr-widget-body media"> - <status-icon :status="iconClass" /> - <div class="media-body"> - <div class="mr-widget-body-controls media space-children"> - <span class="btn-group"> - <button - :disabled="isMergeButtonDisabled" - :class="mergeButtonClass" - type="button" - class="qa-merge-button" - @click="handleMergeButtonClick()" - > - <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> - {{ mergeButtonText }} - </button> - <button - v-if="shouldShowMergeOptionsDropdown" - :disabled="isMergeButtonDisabled" - type="button" - class="btn btn-sm btn-info dropdown-toggle js-merge-moment" - data-toggle="dropdown" - aria-label="Select merge moment" - > - <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> - </button> - <ul - v-if="shouldShowMergeOptionsDropdown" - class="dropdown-menu dropdown-menu-right" - role="menu" - > - <li> - <a - class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" - href="#" - @click.prevent="handleMergeButtonClick(true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> - <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> - </span> - </a> - </li> - <li> - <a - class="accept-merge-request qa-merge-immediately-option" - href="#" - @click.prevent="handleMergeButtonClick(false, true)" - > - <span class="media"> - <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> - <span class="media-body merge-opt-title">Merge immediately</span> - </span> - </a> - </li> - </ul> - </span> - <div class="media-body-wrap space-children"> - <template v-if="shouldShowMergeControls()"> - <label v-if="mr.canRemoveSourceBranch"> - <input - id="remove-source-branch-input" - v-model="removeSourceBranch" - :disabled="isRemoveSourceBranchButtonDisabled" - class="js-remove-source-branch-checkbox" - type="checkbox" - /> - Delete source branch - </label> - - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - v-model="squashBeforeMerge" - :help-path="mr.squashBeforeMergeHelpPath" - :is-disabled="isMergeButtonDisabled" - /> - - <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> - Fast-forward merge without a merge commit - </span> + <div> + <div class="mr-widget-body media"> + <status-icon :status="iconClass" /> + <div class="media-body"> + <div class="mr-widget-body-controls media space-children"> + <span class="btn-group"> <button - v-else :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-sm" + :class="mergeButtonClass" type="button" - @click="toggleCommitMessageEditor" + class="qa-merge-button" + @click="handleMergeButtonClick()" > - Modify commit message + <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + {{ mergeButtonText }} </button> - </template> - <template v-else> - <span class="bold js-resolve-mr-widget-items-message"> - You can only merge once the items above are resolved - </span> - </template> - </div> - </div> - <div v-if="showCommitMessageEditor" class="prepend-top-default commit-message-editor"> - <div class="form-group clearfix"> - <label class="col-form-label" for="commit-message"> Commit message </label> - <div class="col-sm-10"> - <div class="commit-message-container"> - <div class="max-width-marker"></div> - <textarea - id="commit-message" - v-model="commitMessage" - class="form-control js-commit-message" - required="required" - rows="14" - name="Commit message" - ></textarea> - </div> - <p class="hint"> - Try to keep the first line under 52 characters and the others under 72 - </p> - <div class="hint"> - <a href="#" @click.prevent="updateCommitMessage"> {{ commitMessageLinkTitle }} </a> - </div> + <button + v-if="shouldShowMergeOptionsDropdown" + :disabled="isMergeButtonDisabled" + type="button" + class="btn btn-sm btn-info dropdown-toggle js-merge-moment" + data-toggle="dropdown" + aria-label="Select merge moment" + > + <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> + </button> + <ul + v-if="shouldShowMergeOptionsDropdown" + class="dropdown-menu dropdown-menu-right" + role="menu" + > + <li> + <a + class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" + href="#" + @click.prevent="handleMergeButtonClick(true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> + <span class="media-body merge-opt-title">{{ + __('Merge when pipeline succeeds') + }}</span> + </span> + </a> + </li> + <li> + <a + class="accept-merge-request qa-merge-immediately-option" + href="#" + @click.prevent="handleMergeButtonClick(false, true)" + > + <span class="media"> + <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> + <span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span> + </span> + </a> + </li> + </ul> + </span> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls"> + <label v-if="mr.canRemoveSourceBranch"> + <input + id="remove-source-branch-input" + v-model="removeSourceBranch" + :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" + type="checkbox" + /> + {{ __('Delete source branch') }} + </label> + + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" + /> + </template> + <template v-else> + <span class="bold js-resolve-mr-widget-items-message"> + {{ __('You can only merge once the items above are resolved') }} + </span> + </template> </div> </div> </div> </div> + <template v-if="shouldShowMergeControls"> + <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message"> + {{ __('Fast-forward merge without a merge commit') }} + </div> + <template v-else> + <commits-header + :is-squash-enabled="squashBeforeMerge" + :commits-count="mr.commitsCount" + :target-branch="mr.targetBranch" + > + <ul class="border-top content-list commits-list flex-list"> + <commit-edit + v-if="squashBeforeMerge" + v-model="squashCommitMessage" + :label="__('Squash commit message')" + input-id="squash-message-edit" + squash + > + <commit-message-dropdown + slot="header" + v-model="squashCommitMessage" + :commits="mr.commits" + /> + </commit-edit> + <commit-edit + v-model="commitMessage" + :label="__('Merge commit message')" + input-id="merge-message-edit" + > + <label slot="checkbox"> + <input + id="include-description" + type="checkbox" + @change="updateMergeCommitMessage($event.target.checked)" + /> + {{ __('Include merge request description') }} + </label> + </commit-edit> + </ul> + </commits-header> + </template> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 57c4dfbe3b7..abbbe19c5ef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -315,7 +315,7 @@ export default { :endpoint="mr.testResultsPath" /> - <div class="mr-widget-section"> + <div class="mr-widget-section p-0"> <component :is="componentName" :mr="mr" :service="service" /> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 36cac230d9d..58363f632a9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -42,6 +42,8 @@ export default class MergeRequestStore { this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; this.postMergeDeployments = this.postMergeDeployments || []; + this.commits = data.commits_without_merge_commits || []; + this.squashCommitMessage = data.default_squash_commit_message; this.initRebase(data); if (data.issues_links) { diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 8bdb5bf22c2..13eb46437dd 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -40,6 +40,7 @@ export default { toString: date => pikadayToString(date), onSelect: this.selected.bind(this), onClose: this.toggled.bind(this), + firstDay: gon.first_day_of_week, }); this.$el.append(this.calendar.el); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cb449b642e7..c5c3b66438c 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -391,6 +391,11 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .overflow-auto { overflow: auto; } +.d-flex-center { + display: flex; + align-items: center; + justify-content: center; +} /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } @@ -402,6 +407,10 @@ img.emoji { .min-height-0 { min-height: 0; } +.w-3 { width: #{3 * $grid-size}; } + +.h-3 { width: #{3 * $grid-size}; } + /** COMMON SPACING CLASSES **/ .gl-pl-0 { padding-left: 0; } .gl-pl-1 { padding-left: #{0.5 * $grid-size}; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 037a5adfb7e..6108eaa1ad0 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -4,6 +4,7 @@ */ .file-holder { border: 1px solid $border-color; + border-top: 0; border-radius: $border-radius-default; &.file-holder-no-border { @@ -51,6 +52,7 @@ position: absolute; top: 5px; right: 15px; + margin-left: auto; .btn { padding: 0 10px; @@ -324,10 +326,12 @@ span.idiff { &, .file-holder & { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; background-color: $gray-light; border-bottom: 1px solid $border-color; + border-top: 1px solid $border-color; padding: 5px $gl-padding; margin: 0; border-radius: $border-radius-default $border-radius-default 0 0; @@ -365,16 +369,12 @@ span.idiff { margin: 0 10px 0 0; } - .file-actions { - white-space: nowrap; - - .btn { - padding: 0 10px; - font-size: 13px; - line-height: 28px; - display: inline-block; - float: none; - } + .file-actions .btn { + padding: 0 10px; + font-size: 13px; + line-height: 28px; + display: inline-block; + float: none; } @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9eae9a831fa..96dab609a13 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -243,6 +243,7 @@ $gl-padding-8: 8px; $gl-padding: 16px; $gl-padding-24: 24px; $gl-padding-32: 32px; +$gl-padding-50: 50px; $gl-col-padding: 15px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -490,6 +491,7 @@ $builds-trace-bg: #111; */ $commit-max-width-marker-color: rgba(0, 0, 0, 0); $commit-message-text-area-bg: rgba(0, 0, 0, 0); +$commit-stat-summary-height: 36px; /* * Common @@ -664,8 +666,14 @@ $priority-label-empty-state-width: 114px; Issues Analytics */ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15); + /* Merge Requests */ $mr-tabs-height: 51px; $mr-version-controls-height: 56px; + +/* +Compare Branches +*/ +$compare-branches-sticky-header-height: 68px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 02aac58a475..e3b98b26a11 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -7,22 +7,13 @@ cursor: pointer; @media (min-width: map-get($grid-breakpoints, md)) { + $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height; + position: -webkit-sticky; position: sticky; - top: $mr-version-controls-height + $header-height + $mr-tabs-height; - margin-left: -1px; - border-left: 1px solid $border-color; + top: $mr-file-header-top; z-index: 102; - &.is-commit { - top: $header-height + 36px; - - .with-performance-bar & { - top: $header-height + 36px + $performance-bar-height; - - } - } - &::before { content: ''; position: absolute; @@ -35,7 +26,23 @@ } .with-performance-bar & { - top: $header-height + $performance-bar-height + $mr-version-controls-height + $mr-tabs-height; + top: $mr-file-header-top + $performance-bar-height; + } + + &.is-commit { + top: $header-height + $commit-stat-summary-height; + + .with-performance-bar & { + top: $header-height + $commit-stat-summary-height + $performance-bar-height; + } + } + + &.is-compare { + top: $header-height + $compare-branches-sticky-header-height; + + .with-performance-bar & { + top: $performance-bar-height + $header-height + $compare-branches-sticky-header-height; + } } } @@ -501,6 +508,25 @@ } } +.diff-stats { + align-items: center; + padding: 0 .25rem; + + .diff-stats-group { + padding: 0 .25rem; + } + + svg.diff-stats-icon { + vertical-align: text-bottom; + } + + &.is-compare-versions-header { + .diff-stats-group { + padding: 0 .5rem; + } + } +} + .file-content .diff-file { margin: 0; border: 0; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 38a7e199c6a..135730d71e9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -38,9 +38,7 @@ } .mr-widget-section { - .media { - align-items: center; - } + border-radius: $border-radius-default $border-radius-default 0 0; .code-text { flex: 1; @@ -56,6 +54,11 @@ .mr-widget-extension { border-top: 1px solid $border-color; background-color: $gray-light; + + &.clickable:hover { + background-color: $gl-gray-200; + cursor: pointer; + } } .mr-widget-workflow { @@ -78,6 +81,7 @@ border-top: 0; } +.mr-widget-body, .mr-widget-section, .mr-widget-content, .mr-widget-footer { @@ -87,11 +91,38 @@ .mr-state-widget { color: $gl-text-color; + .commit-message-edit { + border-radius: $border-radius-default; + } + .mr-widget-section, .mr-widget-footer { border-top: solid 1px $border-color; } + .mr-fast-forward-message { + padding-left: $gl-padding-50; + padding-bottom: $gl-padding; + } + + .commits-list { + > li { + padding: $gl-padding; + + @include media-breakpoint-up(md) { + padding-left: $gl-padding-50; + } + } + } + + .mr-commit-dropdown { + .dropdown-menu { + @include media-breakpoint-up(md) { + width: 150%; + } + } + } + .mr-widget-footer { padding: 0; } @@ -405,7 +436,7 @@ } .mr-widget-help { - padding: 10px 16px 10px 48px; + padding: 10px 16px 10px $gl-padding-50; font-style: italic; } @@ -423,10 +454,6 @@ } } -.mr-widget-body-controls { - flex-wrap: wrap; -} - .mr_source_commit, .mr_target_commit { margin-bottom: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2342c284a5e..3eb02cd4358 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -946,6 +946,11 @@ pre.light-well { .flex-wrapper { min-width: 0; margin-top: -$gl-padding-8; // negative margin required for flex-wrap + flex: 1 1 100%; + + .project-title { + line-height: 20px; + } } p, @@ -984,14 +989,16 @@ pre.light-well { } .controls { - margin-top: $gl-padding-8; + @include media-breakpoint-down(xs) { + margin-top: $gl-padding-8; + } - @include media-breakpoint-down(md) { + @include media-breakpoint-up(sm) { margin-top: 0; } - @include media-breakpoint-down(xs) { - margin-top: $gl-padding-8; + @include media-breakpoint-up(lg) { + flex: 1 1 40%; } .icon-wrapper { @@ -1041,7 +1048,7 @@ pre.light-well { min-height: 40px; min-width: 40px; - .identicon.s64 { + .identicon.s48 { font-size: 16px; } } diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index b9717b97640..3bd91b71d92 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -127,6 +127,7 @@ class Clusters::ClustersController < Clusters::BaseController params.require(:cluster).permit( :enabled, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :namespace ] @@ -136,6 +137,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :base_domain, platform_kubernetes_attributes: [ :api_url, :token, diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 97120273d6b..cc2bb99f55b 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -116,8 +116,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController session[:service_tickets][provider] = ticket end + def build_auth_user(auth_user_class) + auth_user_class.new(oauth) + end + def sign_in_user_flow(auth_user_class) - auth_user = auth_user_class.new(oauth) + auth_user = build_auth_user(auth_user_class) user = auth_user.find_and_update! if auth_user.valid_sign_in? diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 37ac11dc6a1..94002095739 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -33,12 +33,10 @@ class Profiles::PreferencesController < Profiles::ApplicationController end def preferences_params - params.require(:user).permit( - :color_scheme_id, - :layout, - :dashboard, - :project_view, - :theme_id - ) + params.require(:user).permit(preferences_param_names) + end + + def preferences_param_names + [:color_scheme_id, :layout, :dashboard, :project_view, :theme_id, :first_day_of_week] end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 79685e8b675..e9cd475a199 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -11,11 +11,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index] - before_action do - push_frontend_feature_flag(:area_chart, project) - end - - # Returns all environments or all folders based on the :nested param def index @environments = project.environments .with_state(params[:scope] || :available) diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 9e403e1d25b..88d0755f41f 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -15,6 +15,14 @@ class Projects::ErrorTrackingController < Projects::ApplicationController end end + def list_projects + respond_to do |format| + format.json do + render_project_list_json + end + end + end + private def render_index_json @@ -32,6 +40,32 @@ class Projects::ErrorTrackingController < Projects::ApplicationController } end + def render_project_list_json + service = ErrorTracking::ListProjectsService.new( + project, + current_user, + list_projects_params + ) + result = service.execute + + if result[:status] == :success + render json: { + projects: serialize_projects(result[:projects]) + } + else + return render( + status: result[:http_status] || :bad_request, + json: { + message: result[:message] + } + ) + end + end + + def list_projects_params + params.require(:error_tracking_setting).permit([:api_host, :token]) + end + def set_polling_interval Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) end @@ -41,4 +75,10 @@ class Projects::ErrorTrackingController < Projects::ApplicationController .new(project: project, user: current_user) .represent(errors) end + + def serialize_projects(projects) + ErrorTracking::ProjectSerializer + .new(project: project, user: current_user) + .represent(projects) + end end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 54ff7ded8e5..6045ee4e171 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -34,7 +34,8 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :task_num, :title, :discussion_locked, - label_ids: [] + label_ids: [], + update_task: [:index, :checked, :line_number, :line_source] ] end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 3e08c0ccd8f..23af2e0521c 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -305,7 +305,7 @@ class IssuableFinder def use_subquery_for_search? strong_memoize(:use_subquery_for_search) do attempt_group_search_optimizations? && - Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: false) + Feature.enabled?(:use_subquery_for_group_issues_search, default_enabled: true) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index c8e4e2e3df9..e635f608237 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -150,6 +150,7 @@ module ApplicationSettingsHelper :email_author_in_body, :enabled_git_access_protocol, :enforce_terms, + :first_day_of_week, :gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast, @@ -231,7 +232,8 @@ module ApplicationSettingsHelper :web_ide_clientside_preview_enabled, :diff_max_patch_bytes, :commit_email_hostname, - :protected_ci_variables + :protected_ci_variables, + :local_markdown_version ] end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 516c8a353ea..67e7e475920 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -9,41 +9,4 @@ module AutoDevopsHelper !project.repository.gitlab_ci_yml && !project.ci_service end - - def auto_devops_warning_message(project) - if missing_auto_devops_service?(project) - params = { - kubernetes: link_to('Kubernetes cluster', project_clusters_path(project)) - } - - if missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name and a %{kubernetes} to work correctly.') % params - else - _('Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly.') % params - end - elsif missing_auto_devops_domain?(project) - _('Auto Review Apps and Auto Deploy need a domain name to work correctly.') - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def cluster_ingress_ip(project) - project - .cluster_ingresses - .where("external_ip is not null") - .limit(1) - .pluck(:external_ip) - .first - end - # rubocop: enable CodeReuse/ActiveRecord - - private - - def missing_auto_devops_domain?(project) - !(project.auto_devops || project.build_auto_devops)&.has_domain? - end - - def missing_auto_devops_service?(project) - !project.deployment_platform&.active? - end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index f4f46b0fe96..bc1742e8167 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -43,6 +43,17 @@ module PreferencesHelper ] end + def first_day_of_week_choices + [ + [_('Sunday'), 0], + [_('Monday'), 1] + ] + end + + def first_day_of_week_choices_with_default + first_day_of_week_choices.unshift([_('System default (%{default})') % { default: default_first_day_of_week }, nil]) + end + def user_application_theme @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class end @@ -66,4 +77,8 @@ module PreferencesHelper def excluded_dashboard_choices ['operations'] end + + def default_first_day_of_week + first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first + end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index c4e310e638d..a3d662d8250 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -7,6 +7,12 @@ class ApplicationRecord < ActiveRecord::Base where(id: ids) end + def self.safe_find_or_create_by!(*args) + safe_find_or_create_by(*args).tap do |record| + record.validate! unless record.persisted? + end + end + def self.safe_find_or_create_by(*args) transaction(requires_new: true) do find_or_create_by(*args) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 88746375c67..daadf9427ba 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -193,6 +193,10 @@ class ApplicationSetting < ActiveRecord::Base allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds } + validates :local_markdown_version, + allow_nil: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -246,6 +250,7 @@ class ApplicationSetting < ActiveRecord::Base dsa_key_restriction: 0, ecdsa_key_restriction: 0, ed25519_key_restriction: 0, + first_day_of_week: 0, gitaly_timeout_default: 55, gitaly_timeout_fast: 10, gitaly_timeout_medium: 30, @@ -303,7 +308,8 @@ class ApplicationSetting < ActiveRecord::Base usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, - protected_ci_variables: false + protected_ci_variables: false, + local_markdown_version: 0 } end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index a2c48973fa5..f2f5b89e3bb 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -18,6 +18,7 @@ module Clusters Applications::Knative.application_name => Applications::Knative }.freeze DEFAULT_ENVIRONMENT = '*'.freeze + KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze belongs_to :user @@ -49,7 +50,7 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? @@ -65,6 +66,9 @@ module Clusters delegate :available?, to: :application_ingress, prefix: true, allow_nil: true delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true delegate :available?, to: :application_knative, prefix: true, allow_nil: true + delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true + + alias_attribute :base_domain, :domain enum cluster_type: { instance_type: 1, @@ -193,8 +197,41 @@ module Clusters project_type? end + def kube_ingress_domain + @kube_ingress_domain ||= domain.presence || instance_domain || legacy_auto_devops_domain + end + + def predefined_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + break variables unless kube_ingress_domain + + variables.append(key: KUBE_INGRESS_BASE_DOMAIN, value: kube_ingress_domain) + end + end + private + def instance_domain + @instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain + end + + # To keep backward compatibility with AUTO_DEVOPS_DOMAIN + # environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN + # is set if AUTO_DEVOPS_DOMAIN is set on any of the following options: + # ProjectAutoDevops#Domain, project variables or group variables, + # as the AUTO_DEVOPS_DOMAIN is needed for CI_ENVIRONMENT_URL + # + # This method should be removed on 12.0 + def legacy_auto_devops_domain + if project_type? + project&.auto_devops&.domain.presence || + project.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence || + project.group&.variables&.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + elsif group_type? + group.variables.find_by(key: 'AUTO_DEVOPS_DOMAIN')&.value.presence + end + end + def restrict_modification if provider&.on_creation? errors.add(:base, "cannot modify during creation") diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 8f3424db295..c8969351ed9 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -98,6 +98,8 @@ module Clusters .append(key: 'KUBE_NAMESPACE', value: actual_namespace) .append(key: 'KUBECONFIG', value: kubeconfig, public: false, file: true) end + + variables.concat(cluster.predefined_variables) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 5fa6f79bdaa..1a8570b80c3 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -115,7 +115,28 @@ module CacheMarkdownField end def latest_cached_markdown_version - CacheMarkdownField::CACHE_COMMONMARK_VERSION + @latest_cached_markdown_version ||= (CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) | local_version + end + + def local_version + # because local_markdown_version is stored in application_settings which + # uses cached_markdown_version too, we check explicitly to avoid + # endless loop + return local_markdown_version if has_attribute?(:local_markdown_version) + + settings = Gitlab::CurrentSettings.current_application_settings + + # Following migrations are not properly isolated and + # use real models (by calling .ghost method), in these migrations + # local_markdown_version attribute doesn't exist yet, so we + # use a default value: + # db/migrate/20170825104051_migrate_issues_to_ghost_user.rb + # db/migrate/20171114150259_merge_requests_author_id_foreign_key.rb + if settings.respond_to?(:local_markdown_version) + settings.local_markdown_version + else + 0 + end end included do diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 0816778deae..7f9ff7bbda6 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class GpgSignature < ActiveRecord::Base +class GpgSignature < ApplicationRecord include ShaAttribute sha_attribute :commit_sha @@ -33,6 +33,11 @@ class GpgSignature < ActiveRecord::Base ) end + def self.safe_create!(attributes) + create_with(attributes) + .safe_find_or_create_by!(commit_sha: attributes[:commit_sha]) + end + def gpg_key=(model) case model when GpgKey diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index 34220c1b450..4635fc72dc7 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -96,7 +96,9 @@ class PoolRepository < ActiveRecord::Base @object_pool ||= Gitlab::Git::ObjectPool.new( shard.name, disk_path + '.git', - source_project.repository.raw) + source_project.repository.raw, + source_project.full_path + ) end def inspect diff --git a/app/models/project.rb b/app/models/project.rb index d4e2ed883bc..8f746f6e094 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1288,7 +1288,7 @@ class Project < ActiveRecord::Base # Forked import is handled asynchronously return if forked? && !force - if gitlab_shell.create_repository(repository_storage, disk_path) + if gitlab_shell.create_project_repository(self) repository.after_create true else diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 2253ad7b543..b6c5c7c4c87 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -24,6 +24,11 @@ class ProjectAutoDevops < ActiveRecord::Base domain.present? || instance_domain.present? end + # From 11.8, AUTO_DEVOPS_DOMAIN has been replaced by KUBE_INGRESS_BASE_DOMAIN. + # See Clusters::Cluster#predefined_variables and https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580 + # for more info. + # Support for AUTO_DEVOPS_DOMAIN support will be dropped on 12.0 on + # https://gitlab.com/gitlab-org/gitlab-ce/issues/52363 def predefined_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| if has_domain? diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 559e4f99294..c43bd45a62f 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -60,7 +60,7 @@ class ProjectWiki def wiki @wiki ||= begin gl_repository = Gitlab::GlRepository.gl_repository(project, true) - raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository) + raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository, full_path) create_repo!(raw_repository) unless raw_repository.exists? @@ -175,7 +175,7 @@ class ProjectWiki private def create_repo!(raw_repository) - gitlab_shell.create_repository(project.repository_storage, disk_path) + gitlab_shell.create_wiki_repository(project) raise CouldNotCreateWikiError unless raw_repository.exists? diff --git a/app/models/repository.rb b/app/models/repository.rb index bfd2608bed4..7c50b4488e5 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1104,6 +1104,9 @@ class Repository end def initialize_raw_repository - Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, is_wiki)) + Gitlab::Git::Repository.new(project.repository_storage, + disk_path + '.git', + Gitlab::GlRepository.gl_repository(project, is_wiki), + project.full_path) end end diff --git a/app/models/user.rb b/app/models/user.rb index 9c091ac366c..24101eda0b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -228,6 +228,9 @@ class User < ApplicationRecord delegate :path, to: :namespace, allow_nil: true, prefix: true delegate :notes_filter_for, to: :user_preference delegate :set_notes_filter, to: :user_preference + delegate :first_day_of_week, :first_day_of_week=, to: :user_preference + + accepts_nested_attributes_for :user_preference, update_only: true state_machine :state, initial: :active do event :block do diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb index b2406f4d631..68724088fff 100644 --- a/app/serializers/error_tracking/project_serializer.rb +++ b/app/serializers/error_tracking/project_serializer.rb @@ -2,6 +2,6 @@ module ErrorTracking class ProjectSerializer < BaseSerializer - entity ProjectEntity + entity ErrorTracking::ProjectEntity end end diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb new file mode 100644 index 00000000000..c6e8be0f2be --- /dev/null +++ b/app/services/error_tracking/list_projects_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module ErrorTracking + class ListProjectsService < ::BaseService + def execute + return error('access denied') unless can_read? + + setting = project_error_tracking_setting + + unless setting.valid? + return error(setting.errors.full_messages.join(', '), :bad_request) + end + + begin + result = setting.list_sentry_projects + rescue Sentry::Client::Error => e + return error(e.message, :bad_request) + rescue Sentry::Client::SentryError => e + return error(e.message, :unprocessable_entity) + end + + success(projects: result[:projects]) + end + + private + + def project_error_tracking_setting + (project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting| + setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( + api_host: params[:api_host], + organization_slug: nil, + project_slug: nil + ) + + setting.token = params[:token] + setting.enabled = true + end + end + + def can_read? + can?(current_user, :read_sentry_issue, project) + end + end +end diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index e563447c64c..be33947d0eb 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -8,6 +8,7 @@ module Labels # returns the updated label def execute(label) + params[:name] = params.delete(:new_name) if params.key?(:new_name) params[:color] = convert_color_name_to_hex if params[:color].present? label.update(params) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 86a04587f79..8112c2a4299 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -21,7 +21,7 @@ module MergeRequests end handle_wip_event(merge_request) - update(merge_request) + update_task_event(merge_request) || update(merge_request) end # rubocop:disable Metrics/AbcSize @@ -83,6 +83,11 @@ module MergeRequests end # rubocop:enable Metrics/AbcSize + def handle_task_changes(merge_request) + todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.update_merge_request(merge_request, current_user) + end + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 5861b803996..7214e9efaf6 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -73,7 +73,7 @@ module Projects project.ensure_repository project.repository.fetch_as_mirror(project.import_url, refmap: refmap) else - gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url) + gitlab_shell.import_project_repository(project) end rescue Gitlab::Shell::Error => e # Expire cache to prevent scenarios such as: diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb index b5c4cd3235d..cfe187d9b12 100644 --- a/app/services/task_list_toggle_service.rb +++ b/app/services/task_list_toggle_service.rb @@ -32,7 +32,8 @@ class TaskListToggleService source_line_index = line_number - 1 markdown_task = source_lines[source_line_index] - return unless markdown_task == line_source + # The source in the DB could be using either \n or \r\n line endings + return unless markdown_task == line_source || markdown_task == line_source + "\r" return unless source_checkbox = Taskable::ITEM_PATTERN.match(markdown_task) currently_checked = TaskList::Item.new(source_checkbox[1]).complete? diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml new file mode 100644 index 00000000000..95d016a94a5 --- /dev/null +++ b/app/views/admin/application_settings/_localization.html.haml @@ -0,0 +1,11 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-localization-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :first_day_of_week, _('Default first day of the week'), class: 'label-bold' + = f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control' + .form-text.text-muted + = _('Default first day of the week in calendars and date pickers.') + + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml index 00000b86ab7..c468d69d7b8 100644 --- a/app/views/admin/application_settings/preferences.html.haml +++ b/app/views/admin/application_settings/preferences.html.haml @@ -56,3 +56,14 @@ = _('Configure Gitaly timeouts.') .settings-content = render 'gitaly' + +%section.settings.as-localization.no-animate#js-localization-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Localization') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various localization settings.') + .settings-content + = render 'localization' diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 4c47e11927e..7acd9ce0562 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -20,12 +20,27 @@ .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") - else = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true - - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain') - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } .form-text.text-muted %code * = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } + .form-group + %h5= s_('ClusterIntegration|Base domain') + = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus' + .form-text.text-muted + - auto_devops_url = help_page_path('topics/autodevops/index') + - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } + = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe } + - if @cluster.application_ingress_external_ip.present? + = s_('ClusterIntegration|Alternatively') + %code #{@cluster.application_ingress_external_ip}.nip.io + = s_('ClusterIntegration| can be used instead of a custom domain.') + - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-cluster-ip') + - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url } + = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe } + - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index bf475c07711..3fbaaafe89e 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -36,7 +36,7 @@ %span = _('Activity') - = render_if_exists 'groups/sidebar/security_dashboard' + = render_if_exists 'groups/sidebar/security_dashboard' # EE-specific - if group_sidebar_link?(:contribution_analytics) = nav_link(path: 'analytics#show') do diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 7c378633667..1a9aca1f6bf 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -51,11 +51,30 @@ = f.label :dashboard, class: 'label-bold' do Default dashboard = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + + = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific + .form-group = f.label :project_view, class: 'label-bold' do Project overview content = f.select :project_view, project_view_choices, {}, class: 'form-control' .form-text.text-muted Choose what content you want to see on a project’s overview page. + + .col-sm-12 + %hr + + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + = _('Localization') + %p + = _('Customize language and region related settings.') + = succeed '.' do + = link_to _('Learn more'), help_page_path('user/profile/preferences', anchor: 'localization'), target: '_blank' + .col-lg-8 + .form-group + = f.label :first_day_of_week, class: 'label-bold' do + = _('First day of the week') + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' .form-group - = f.submit 'Save changes', class: 'btn btn-success' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 06f0cd9675e..fe9a8ac4182 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -10,7 +10,7 @@ .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" = render "ci_menu" - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-commit" .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index b6bebbabed0..5774b48a054 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-compare" - else .card.bg-light .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index cc2d0d3b2d8..2dba3fcd664 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,7 +2,7 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files -- is_commit = local_assigns.fetch(:is_commit, false) +- diff_page_context = local_assigns.fetch(:diff_page_context, nil) .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .files-changed-inner @@ -25,4 +25,4 @@ = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, is_commit: is_commit } + = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 5565ae1d98b..855b719dc45 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,11 +1,11 @@ - environment = local_assigns.fetch(:environment, nil) -- is_commit = local_assigns.fetch(:is_commit, false) +- diff_page_context = local_assigns.fetch(:diff_page_context, nil) - file_hash = hexdigest(diff_file.file_path) - image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' - image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_file.content_sha) } - .js-file-title.file-title-flex-parent{ class: is_commit ? "is-commit" : "" } + .js-file-title.file-title-flex-parent{ class: diff_page_context } .file-header-content = render "projects/diffs/file_header", diff_file: diff_file, url: "##{file_hash}" diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 5ec5a06396e..8c4d1c32ebe 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -4,10 +4,6 @@ = form_errors(@project) %fieldset.builds-feature.js-auto-devops-settings .form-group - - message = auto_devops_warning_message(@project) - - if message - %p.auto-devops-warning-message.settings-message.text-center - = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .card.auto-devops-card .card-body @@ -21,19 +17,12 @@ = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' .card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' } - = form.label :domain do - %strong= _('Domain') - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') - - if cluster_ingress_ip = cluster_ingress_ip(@project) - = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' - + %p.settings-message.text-center + - kubernetes_cluster_link = help_page_path('user/project/clusters/index') + - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } + = s_('CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_start: kubernetes_cluster_start, kubernetes_cluster_end: '</a>'.html_safe } %label.prepend-top-10 %strong= s_('CICD|Deployment strategy') - %p.settings-message.text-center - = s_('CICD|Deployment strategy needs a domain name to work correctly.') .form-check = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input' = form.label :deploy_strategy_continuous, class: 'form-check-label' do diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index e1564d57426..df17ae95e2a 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,21 +12,20 @@ - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) -- css_details_class = compact_mode ? "d-flex flex-column flex-sm-row flex-md-row align-items-sm-center" : "align-items-center flex-md-fill flex-lg-column d-sm-flex d-lg-block" -- css_controls_class = compact_mode ? "" : "align-items-md-end align-items-lg-center flex-lg-row" +- css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between" %li.project-row.d-flex{ class: css_class } = cache(cache_key) do - if avatar - .avatar-container.s64.flex-grow-0.flex-shrink-0 + .avatar-container.s48.flex-grow-0.flex-shrink-0 = link_to project_path(project), class: dom_class(project) do - if project.creator && use_creator_avatar - = image_tag avatar_icon_for_user(project.creator, 64), class: "avatar s65", alt:'' + = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s65", alt:'' - else - = project_icon(project, alt: '', class: 'avatar project-avatar s64', width: 64, height: 64) - .project-details.flex-sm-fill{ class: css_details_class } - .flex-wrapper.flex-fill - .d-flex.align-items-center.flex-wrap + = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48) + .project-details.d-sm-flex.flex-sm-fill.align-items-center + .flex-wrapper + .d-flex.align-items-center.flex-wrap.project-title %h2.d-flex.prepend-top-8 = link_to project_path(project), class: 'text-plain' do %span.project-full-name.append-right-8>< @@ -52,13 +51,13 @@ %span.user-access-role.d-block= Gitlab::Access.human_access(access) - if show_last_commit_as_description - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") - elsif project.description.present? - .description.d-none.d-sm-block.prepend-top-8.append-right-default + .description.d-none.d-sm-block.append-right-default = markdown_field(project, :description) - .controls.d-flex.flex-row.flex-sm-column.flex-md-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } + .controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0{ class: css_controls_class } .icon-container.d-flex.align-items-center - if project.archived %span.d-flex.icon-wrapper.badge.badge-warning archived @@ -74,13 +73,13 @@ = number_with_delimiter(project.forks_count) - if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode) = link_to project_merge_requests_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper merge-requests has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip", title: _('Merge Requests'), data: { container: 'body', placement: 'top' } do = sprite_icon('git-merge', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_merge_requests_count) - if show_issue_count?(disabled: !issues, compact_mode: compact_mode) = link_to project_issues_path(project), - class: "d-none d-lg-flex align-items-center icon-wrapper issues has-tooltip", + class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip", title: _('Issues'), data: { container: 'body', placement: 'top' } do = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) @@ -89,19 +88,3 @@ = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') .updated-note %span Updated #{updated_tooltip} - - .d-none.d-lg-flex.align-item-stretch - - unless compact_mode - - if current_user - %button.star-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(project, :json) } } - - if current_user.starred?(project) - = sprite_icon('star', { css_class: 'icon' }) - %span.starred= s_('ProjectOverview|Unstar') - - else - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') - - - else - = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do - = sprite_icon('star-o', { css_class: 'icon' }) - %span= s_('ProjectOverview|Star') diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 7eae07d3f6b..a9b88a133be 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,19 +15,19 @@ class RepositoryForkWorker return target_project.import_state.mark_as_failed(_('Source project cannot be found.')) end - fork_repository(target_project, source_project.repository_storage, source_project.disk_path) + fork_repository(target_project, source_project) end private - def fork_repository(target_project, source_repository_storage_name, source_disk_path) + def fork_repository(target_project, source_project) return unless start_fork(target_project) Gitlab::Metrics.add_event(:fork_repository) - result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path, - target_project.repository_storage, target_project.disk_path) - raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result + result = gitlab_shell.fork_repository(source_project, target_project) + + raise "Unable to fork project #{target_project.id} for repository #{source_project.disk_path} -> #{target_project.disk_path}" unless result target_project.after_import end |