diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-09-07 00:06:55 +0300 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-09-07 00:06:55 +0300 |
commit | 915306ec50c50b3b1b0793150a7cb4f31fbbee75 (patch) | |
tree | c9f6a045ee02003ca9b715783692cb1d623f619d /app | |
parent | c353773787407158b6319b6944dee57a6e3522b9 (diff) | |
parent | 7aa6766cd15036dcfb5391b3fd7bffb9069ffbcf (diff) |
Merge branch 'master' into 'alerts-for-built-in-metrics'
# Conflicts:
# db/schema.rb
Diffstat (limited to 'app')
176 files changed, 1607 insertions, 807 deletions
diff --git a/app/assets/images/auth_buttons/auth0_64.png b/app/assets/images/auth_buttons/auth0_64.png Binary files differnew file mode 100644 index 00000000000..5ad59659380 --- /dev/null +++ b/app/assets/images/auth_buttons/auth0_64.png diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png Binary files differindex 85de7793440..168a9c81395 100644 --- a/app/assets/images/auth_buttons/azure_64.png +++ b/app/assets/images/auth_buttons/azure_64.png diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png Binary files differindex b3d022a5a70..0edf7f52a11 100644 --- a/app/assets/images/auth_buttons/bitbucket_64.png +++ b/app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png Binary files differindex 720824230a5..389c1cd54ca 100644 --- a/app/assets/images/auth_buttons/google_64.png +++ b/app/assets/images/auth_buttons/google_64.png diff --git a/app/assets/images/auth_buttons/jwt_64.png b/app/assets/images/auth_buttons/jwt_64.png Binary files differnew file mode 100644 index 00000000000..ca97ae47002 --- /dev/null +++ b/app/assets/images/auth_buttons/jwt_64.png diff --git a/app/assets/images/auth_buttons/shibboleth_64.png b/app/assets/images/auth_buttons/shibboleth_64.png Binary files differnew file mode 100644 index 00000000000..d4c752f9400 --- /dev/null +++ b/app/assets/images/auth_buttons/shibboleth_64.png diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 0fdf0c7a389..ebf76af5966 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -1,16 +1,12 @@ import Visibility from 'visibilityjs'; import Vue from 'vue'; +import initDismissableCallout from '~/dismissable_callout'; import { s__, sprintf } from '../locale'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; import initSettingsPanels from '../settings_panels'; import eventHub from './event_hub'; -import { - APPLICATION_STATUS, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from './constants'; +import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; @@ -66,6 +62,7 @@ export default class Clusters { this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); + initDismissableCallout('.js-cluster-security-warning'); initSettingsPanels(); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); @@ -129,7 +126,8 @@ export default class Clusters { if (!Visibility.hidden()) { this.poll.makeRequest(); } else { - this.service.fetchData() + this.service + .fetchData() .then(data => this.handleSuccess(data)) .catch(() => Clusters.handleError()); } @@ -177,15 +175,21 @@ export default class Clusters { checkForNewInstalls(prevApplicationMap, newApplicationMap) { const appTitles = Object.keys(newApplicationMap) - .filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && - prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && - prevApplicationMap[appId].status !== null) + .filter( + appId => + newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && + prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && + prevApplicationMap[appId].status !== null, + ) .map(appId => newApplicationMap[appId].title); if (appTitles.length > 0) { - const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), { - appList: appTitles.join(', '), - }); + const text = sprintf( + s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), + { + appList: appTitles.join(', '), + }, + ); Flash(text, 'notice', this.successApplicationContainer); } } @@ -218,13 +222,18 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestReason', null); - this.service.installApplication(appId, data.params) + this.service + .installApplication(appId, data.params) .then(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); }) .catch(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); - this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); + this.store.updateAppProperty( + appId, + 'requestReason', + s__('ClusterIntegration|Request to begin installing failed'), + ); }); } diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 1e5c733d151..789c8360124 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,14 +1,14 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import setupToggleButtons from '~/toggle_buttons'; -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +import initDismissableCallout from '~/dismissable_callout'; import ClustersService from './services/clusters_service'; export default () => { const clusterList = document.querySelector('.js-clusters-list'); - gcpSignupOffer(); + initDismissableCallout('.gcp-signup-offer'); // The empty state won't have a clusterList if (clusterList) { diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue index 045688a32bf..0ec6b8b7f21 100644 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue @@ -63,7 +63,7 @@ export default { v-else role="button" class="fa fa-times dropdown-input-search" - @click="clearSearch" + @click.stop.prevent="clearSearch" ></i> </div> <div class="dropdown-content"> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index caf84dc9573..6348f32d36d 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -44,10 +44,9 @@ export default { class="notes_holder" > <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> + class="notes_content" + colspan="3" + > <div class="content"> <diff-discussions v-if="discussions.length" diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0522e32c410..bc69ae30777 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -107,7 +107,6 @@ export default { }, [types.EXPAND_ALL_FILES](state) { - // eslint-disable-next-line no-param-reassign state.diffFiles = state.diffFiles.map(file => ({ ...file, collapsed: false, diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/dismissable_callout.js index 8bc20a1c09f..5185b019376 100644 --- a/app/assets/javascripts/clusters/components/gcp_signup_offer.js +++ b/app/assets/javascripts/dismissable_callout.js @@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import Flash from '~/flash'; -export default function gcpSignupOffer() { - const alertEl = document.querySelector('.gcp-signup-offer'); +export default function initDismissableCallout(alertSelector) { + const alertEl = document.querySelector(alertSelector); if (!alertEl) { return; } diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 8b4f3b05ee7..f820f0dc3f0 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -65,8 +65,8 @@ export const hideMenu = (el) => { const parentEl = el.parentNode; - el.style.display = ''; // eslint-disable-line no-param-reassign - el.style.transform = ''; // eslint-disable-line no-param-reassign + el.style.display = ''; + el.style.transform = ''; el.classList.remove(IS_ABOVE_CLASS); parentEl.classList.remove(IS_OVER_CLASS); parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index adde8c8cdb3..73ae928b0d9 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -37,7 +37,7 @@ </script> <template> - <div class="groups-list-tree-container"> + <div class="groups-list-tree-container qa-groups-list-tree-container"> <div v-if="searchEmpty" class="has-no-search-results" diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js index 081ec2d4c28..0a455f4500f 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js index e413e61eaaa..674782a28ca 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 98102a68e08..0eba9c39817 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 5a2213bbe89..b4be100cb07 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import { normalizeJob } from './utils'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 0347f803757..f2bb87ac674 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index a937fb157f8..66f29824898 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; import { sortTree } from '../utils'; import { diffModes } from '../../constants'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 2a451ef0cd1..cd12ef87d40 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -1,5 +1,3 @@ -/* eslint-disable no-param-reassign */ - import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 8124ae6201f..1e168742667 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1074,7 +1074,7 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 27ff7dea909..802be022ba6 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -148,10 +148,9 @@ export default { </tr> <tr class="notes_holder"> <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> + class="notes_content" + colspan="3" + > <slot></slot> </td> </tr> diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 47bd70537f1..069f8ce55d0 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,8 +1,13 @@ import initSettingsPanels from '~/settings_panels'; import projectSelect from '~/project_select'; +import UsagePingPayload from './usage_ping_payload'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); projectSelect(); + new UsagePingPayload( + document.querySelector('.js-usage-ping-payload-trigger'), + document.querySelector('.js-usage-ping-payload'), + ).init(); }); diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js new file mode 100644 index 00000000000..9a1bc46bf4a --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js @@ -0,0 +1,62 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + +export default class UsagePingPayload { + constructor(trigger, container) { + this.trigger = trigger; + this.container = container; + this.isVisible = false; + this.isInserted = false; + } + + init() { + this.spinner = this.trigger.querySelector('.js-spinner'); + this.text = this.trigger.querySelector('.js-text'); + + this.trigger.addEventListener('click', event => { + event.preventDefault(); + + if (this.isVisible) return this.hidePayload(); + + return this.requestPayload(); + }); + } + + requestPayload() { + if (this.isInserted) return this.showPayload(); + + this.spinner.classList.add('d-inline'); + + return axios + .get(this.container.dataset.endpoint, { + responseType: 'text', + }) + .then(({ data }) => { + this.spinner.classList.remove('d-inline'); + this.insertPayload(data); + }) + .catch(() => { + this.spinner.classList.remove('d-inline'); + flash(__('Error fetching usage ping data.')); + }); + } + + hidePayload() { + this.isVisible = false; + this.container.classList.add('d-none'); + this.text.textContent = __('Preview payload'); + } + + showPayload() { + this.isVisible = true; + this.container.classList.remove('d-none'); + this.text.textContent = __('Hide payload'); + } + + insertPayload(data) { + this.isInserted = true; + this.container.innerHTML = data; + this.showPayload(); + } +} diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js deleted file mode 100644 index 2d5020dbef4..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initUsagePing from './usage_ping'; - -document.addEventListener('DOMContentLoaded', initUsagePing); diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js deleted file mode 100644 index 914a9661c27..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js +++ /dev/null @@ -1,13 +0,0 @@ -import axios from '../../../lib/utils/axios_utils'; -import { __ } from '../../../locale'; -import flash from '../../../flash'; - -export default function UsagePing() { - const el = document.querySelector('.usage-data'); - - axios.get(el.dataset.endpoint, { - responseType: 'text', - }).then(({ data }) => { - el.innerHTML = data; - }).catch(() => flash(__('Error fetching usage ping data.'))); -} diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index cc0e6553e83..9c074b74c3b 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,4 +1,4 @@ -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +import initDismissableCallout from '~/dismissable_callout'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; import Project from './project'; import ShortcutsNavigation from '../../shortcuts_navigation'; @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - gcpSignupOffer(); + initDismissableCallout('.gcp-signup-offer'); initGkeDropdowns(); } diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index a853624e944..fdcbcc236c1 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -13,40 +13,52 @@ export default class Project { constructor() { const $cloneOptions = $('ul.clone-options-dropdown'); const $projectCloneField = $('#project_clone'); - const $cloneBtnText = $('a.clone-dropdown-btn span'); + const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); - const selectedCloneOption = $cloneBtnText.text().trim(); + const selectedCloneOption = $cloneBtnLabel.text().trim(); if (selectedCloneOption.length > 0) { $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - $('a', $cloneOptions).on('click', (e) => { + $('a', $cloneOptions).on('click', e => { + e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); - const activeText = $this.find('.dropdown-menu-inner-title').text(); + const cloneType = $this.data('cloneType'); - e.preventDefault(); + $('.is-active', $cloneOptions).removeClass('is-active'); + $(`a[data-clone-type="${cloneType}"]`).each(function() { + const $el = $(this); + const activeText = $el.find('.dropdown-menu-inner-title').text(); + const $container = $el.closest('.project-clone-holder'); + const $label = $container.find('.js-clone-dropdown-label'); - $('.is-active', $cloneOptions).not($this).removeClass('is-active'); - $this.toggleClass('is-active'); - $projectCloneField.val(url); - $cloneBtnText.text(activeText); + $el.toggleClass('is-active'); + $label.text(activeText); + }); - return $('.clone').text(url); + $projectCloneField.val(url); + $('.js-git-empty .js-clone').text(url); }); // Ref switcher Project.initRefSwitcher(); $('.project-refs-select').on('change', function() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); }); $('.hide-no-ssh-message').on('click', function(e) { Cookies.set('hide_no_ssh_message', 'false'); - $(this).parents('.no-ssh-key-message').remove(); + $(this) + .parents('.no-ssh-key-message') + .remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { Cookies.set('hide_no_password_message', 'false'); - $(this).parents('.no-password-message').remove(); + $(this) + .parents('.no-password-message') + .remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); @@ -58,7 +70,7 @@ export default class Project { } static changeProject(url) { - return window.location = url; + return (window.location = url); } static initRefSwitcher() { @@ -73,14 +85,15 @@ export default class Project { selected = $dropdown.data('selected'); return $dropdown.glDropdown({ data(term, callback) { - axios.get($dropdown.data('refsUrl'), { - params: { - ref: $dropdown.data('ref'), - search: term, - }, - }) - .then(({ data }) => callback(data)) - .catch(() => flash(__('An error occurred while getting projects'))); + axios + .get($dropdown.data('refsUrl'), { + params: { + ref: $dropdown.data('ref'), + search: term, + }, + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('An error occurred while getting projects'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index b76f2f76449..0507f67843f 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; import GpgBadges from '~/gpg_badges'; +import initReadMore from '~/read_more'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; document.addEventListener('DOMContentLoaded', () => { + initReadMore(); new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new - new UserCallout({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new UserCallout({ setCalloutPerProject: false, className: 'js-autodevops-banner', }); diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 29b347824de..a39cc265601 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -132,10 +132,8 @@ export default { if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { - // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { - // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js new file mode 100644 index 00000000000..d2d1ac8c76a --- /dev/null +++ b/app/assets/javascripts/read_more.js @@ -0,0 +1,41 @@ +/** + * ReadMore + * + * Adds "read more" functionality to elements. + * + * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class + * "is-expanded" to the previous element in order to provide a click to expand functionality. + * + * This is useful for long text elements that you would like to truncate, especially for mobile. + * + * Example Markup + * <div class="read-more-container"> + * <p>Some text that should be long enough to have to truncate within a specified container.</p> + * <p>This text will not appear in the container, as only the first line can be truncated.</p> + * <p>This should also not appear, if everything is working correctly!</p> + * </div> + * <button class="js-read-more-trigger">Read more</button> + * + */ +export default function initReadMore(triggerSelector = '.js-read-more-trigger') { + const triggerEls = document.querySelectorAll(triggerSelector); + + if (!triggerEls) return; + + triggerEls.forEach(triggerEl => { + const targetEl = triggerEl.previousElementSibling; + + if (!targetEl) { + return; + } + + triggerEl.addEventListener( + 'click', + e => { + targetEl.classList.add('is-expanded'); + e.target.remove(); + }, + { once: true }, + ); + }); +} diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index 1983a8c9e56..b88bff97075 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 72bd28ae03f..4c3f8dff3c4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -4,6 +4,7 @@ import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; export default { @@ -13,6 +14,9 @@ export default { clipboardButton, TooltipOnTruncate, }, + directives: { + tooltip, + }, props: { mr: { type: Object, @@ -40,10 +44,19 @@ export default { }); }, webIdePath() { - return mergeUrlParams({ - target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? - this.mr.targetProjectFullPath : '', - }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); + if (this.mr.canPushToSourceBranch) { + return mergeUrlParams({ + target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? + this.mr.targetProjectFullPath : '', + }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); + } + + return null; + }, + ideButtonTitle() { + return !this.mr.canPushToSourceBranch + ? s__('mrWidget|You are not allowed to edit this project directly. Please fork to make changes.') + : ''; }, }, }; @@ -93,13 +106,22 @@ export default { v-if="mr.isOpen" class="branch-actions" > - <a - v-if="!mr.sourceBranchRemoved" - :href="webIdePath" - class="btn btn-default inline js-web-ide d-none d-md-inline-block" + <span + v-tooltip + :title="ideButtonTitle" + data-placement="bottom" + tabindex="0" > - {{ s__("mrWidget|Open in Web IDE") }} - </a> + <a + v-if="!mr.sourceBranchRemoved" + :href="webIdePath" + :class="{ disabled: !mr.canPushToSourceBranch }" + class="btn btn-default inline js-web-ide d-none d-md-inline-block" + role="button" + > + {{ s__("mrWidget|Open in Web IDE") }} + </a> + </span> <button :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 73b9131e5ba..b9693892f45 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => { response.headers.forEach((value, key) => { headers[key] = value; }); - + // eslint-disable-next-line no-param-reassign response.headers = headers; }); }); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b1a20c06910..39ffabb3ea6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -64,3 +64,4 @@ @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; @import 'framework/terms'; +@import 'framework/read_more'; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index a265e4206f1..702276780e9 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -229,8 +229,8 @@ svg { margin-bottom: 1px; - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; border-radius: 50%; path { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 72b4ed0ac33..e91e830fcac 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -149,7 +149,8 @@ &.btn-success, &.btn-new, &.btn-create, - &.btn-save { + &.btn-save, + &.btn-register { @include btn-green; } @@ -172,8 +173,7 @@ } &.btn-info, - &.btn-primary, - &.btn-register { + &.btn-primary { @include btn-blue; } @@ -248,7 +248,7 @@ .btn-terminal { svg { height: 14px; - width: 18px; + width: $default-icon-size; } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index abfe350677e..a52e6c4f6a7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -216,8 +216,8 @@ vertical-align: inherit; img { - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 6244fb86fea..6d20c46b99d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -4,11 +4,6 @@ margin-top: 20px; } - .container-fluid { - padding-left: 5px; - padding-right: 5px; - } - .nav-links > li > a { padding: 10px; font-size: 12px; @@ -49,12 +44,8 @@ .project-repo-buttons { display: block; - .count-buttons .btn { - margin: 0 10px; - } - - .count-buttons .count-with-arrow { - display: none; + .count-buttons .count-badge { + margin-top: $gl-padding-8; } } } diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss new file mode 100644 index 00000000000..b84b6e0b256 --- /dev/null +++ b/app/assets/stylesheets/framework/read_more.scss @@ -0,0 +1,13 @@ +.read-more-container { + @include media-breakpoint-down(md) { + &:not(.is-expanded) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > * { + display: inline; + } + } + } +} diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 20394cc1e52..43aaf198609 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -56,8 +56,8 @@ &, .toggle-icon-svg { - width: 18px; - height: 18px; + width: $default-icon-size; + height: $default-icon-size; } .toggle-icon-svg { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d76f5cbd9ff..f5e7a84d082 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -250,7 +250,7 @@ $container-text-max-width: 540px; $gl-avatar-size: 40px; $border-radius-default: 4px; $border-radius-small: 2px; -$settings-icon-size: 18px; +$default-icon-size: 18px; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; @@ -271,6 +271,7 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; +$project-title-row-height: 24px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 6c555aee20a..f0acb78f731 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -4,3 +4,7 @@ padding-bottom: 46px; } } + +.usage-data { + max-height: 400px; +} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7d7143631f2..d673b59e1c0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -749,6 +749,10 @@ left: $gl-padding; } + .dropdown-input .dropdown-input-search { + pointer-events: all; + } + .diff-changed-file { display: flex; padding-top: 8px; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index c9e5fb9c579..fa0ab1a3bae 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -100,6 +100,22 @@ p { margin: 0; } + + .omniauth-btn { + margin-bottom: $gl-padding; + width: 48%; + padding: $gl-padding-8; + + @include media-breakpoint-down(md) { + width: 100%; + } + + img { + width: $default-icon-size; + height: $default-icon-size; + margin-right: $gl-padding; + } + } } .new-session-tabs { @@ -169,10 +185,6 @@ } } - label { - font-weight: $gl-font-weight-normal; - } - .submit-container { margin-top: 16px; } @@ -200,15 +212,6 @@ } } -.oauth-image-link { - margin-right: 10px; - - img { - width: 32px; - height: 32px; - } -} - .devise-layout-html { margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index dbe9f0c03fb..b1e33196049 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -334,20 +334,6 @@ ul.notes { border: 1px solid $white-normal; border-left: 0; - &.notes_line { - vertical-align: middle; - text-align: center; - padding: 10px 0; - background: $gray-light; - color: $text-color; - } - - &.notes_line2 { - text-align: center; - padding: 10px 0; - border-left: 1px solid $note-line2-border !important; - } - &.notes_content { background-color: $gray-light; border-width: 1px 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a95e78931b1..9b7051924e6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -115,7 +115,7 @@ .project-feature-controls { display: flex; align-items: center; - margin: 8px 0; + margin: $gl-padding-8 0; max-width: 432px; .toggle-wrapper { @@ -144,12 +144,8 @@ .group-home-panel { padding-top: 24px; padding-bottom: 24px; + border-bottom: 1px solid $border-color; - @include media-breakpoint-up(sm) { - border-bottom: 1px solid $border-color; - } - - .project-avatar, .group-avatar { float: none; margin: 0 auto; @@ -175,7 +171,6 @@ } } - .project-home-desc, .group-home-desc { margin-left: auto; margin-right: auto; @@ -199,6 +194,62 @@ } } +.project-home-panel { + padding-top: $gl-padding-8; + padding-bottom: $gl-padding-24; + + .project-title-row { + margin-right: $gl-padding-8; + } + + .project-avatar { + width: $project-title-row-height; + height: $project-title-row-height; + flex-shrink: 0; + flex-basis: $project-title-row-height; + margin: 0 $gl-padding-8 0 0; + } + + .project-title { + font-size: 20px; + line-height: $project-title-row-height; + font-weight: bold; + } + + .project-metadata { + font-weight: normal; + font-size: 14px; + line-height: $gl-btn-line-height; + color: $gl-text-color-secondary; + + .icon { + margin-right: $gl-padding-4; + font-size: 16px; + } + + .project-visibility, + .project-license, + .project-tag-list { + margin-right: $gl-padding-8; + } + + .project-license { + .btn { + line-height: 0; + border-width: 0; + } + } + + .project-tag-list, + .project-license { + .icon { + position: relative; + top: 2px; + } + } + } +} + .nav > .project-repo-buttons { margin-top: 0; } @@ -206,8 +257,6 @@ .project-repo-buttons, .group-buttons { .btn { - padding: 3px 10px; - &:last-child { margin-left: 0; } @@ -222,11 +271,15 @@ .fa-caret-down { margin-left: 3px; + + &.dropdown-btn-icon { + margin-left: 0; + } } } .project-action-button { - margin: 15px 5px 0; + margin: $gl-padding $gl-padding-8 0 0; vertical-align: top; } @@ -243,82 +296,45 @@ .count-buttons { display: inline-block; vertical-align: top; - margin-top: 15px; - } + margin-top: $gl-padding; - .project-clone-holder { - display: inline-block; - margin: 15px 5px 0 0; + .count-badge { + height: $input-height; - input { - height: 28px; + .icon { + top: -1px; + } } - } - .count-with-arrow { - display: inline-block; - position: relative; - margin-left: 4px; + .count-badge-count, + .count-badge-button { + border: 1px solid $border-color; + line-height: 1; + } - .arrow { - &::before { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 0; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $count-arrow-border; - pointer-events: none; - } + .count, + .count-badge-button { + color: $gl-text-color; + } - &::after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 1px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - pointer-events: none; - } + .count-badge-count { + padding: 0 12px; + border-right: 0; + border-radius: $border-radius-base 0 0 $border-radius-base; + background: $gray-light; } - .count { - @include btn-white; - display: inline-block; - background: $white-light; - border-radius: 2px; - border-width: 1px; - border-style: solid; - font-size: 13px; - font-weight: $gl-font-weight-bold; - line-height: 13px; - letter-spacing: 0.4px; - padding: 6px 14px; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - background-image: none; - white-space: nowrap; - margin: 0 10px 0 4px; + .count-badge-button { + border-radius: 0 $border-radius-base $border-radius-base 0; + } + } - a { - color: inherit; - } + .project-clone-holder { + display: inline-block; + margin: $gl-padding $gl-padding-8 0 0; - &:hover { - background: $white-light; - } + input { + height: $input-height; } } @@ -333,6 +349,14 @@ min-width: 320px; } } + + .mobile-git-clone { + margin-top: $gl-padding-8; + + .dropdown-menu-inner-content { + @extend .monospace; + } + } } .split-one { @@ -511,7 +535,6 @@ .controls { margin-left: auto; } - } .choose-template { @@ -574,7 +597,7 @@ flex-wrap: wrap; .btn { - padding: 8px; + padding: $gl-padding-8; margin-right: 10px; } @@ -651,7 +674,7 @@ left: -10px; top: 50%; z-index: 10; - padding: 8px 0; + padding: $gl-padding-8 0; text-align: center; background-color: $white-light; color: $gl-text-color-tertiary; @@ -665,7 +688,7 @@ left: 50%; top: 0; transform: translateX(-50%); - padding: 0 8px; + padding: 0 $gl-padding-8; } } @@ -699,17 +722,51 @@ .project-stats { font-size: 0; text-align: center; - max-width: 100%; border-bottom: 1px solid $border-color; - .nav { - margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8; + .scrolling-tabs-container { + .scrolling-tabs { + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; + flex-wrap: wrap; + border-bottom: 0; + } + .fade-left, + .fade-right { + top: 0; + height: 100%; + + .fa { + top: 50%; + margin-top: -$gl-padding-8; + } + } + + .nav { + flex-basis: 100%; + + + .nav { + margin: $gl-padding-8 0; + } + } + + @include media-breakpoint-down(md) { + flex-direction: column; + + .nav { + flex-wrap: nowrap; + } + + .nav:first-child { + margin-right: $gl-padding-8; + } + } + } + + .nav { > li { display: inline-block; - margin-top: $gl-padding-4; - margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -732,13 +789,17 @@ font-size: $gl-font-size; line-height: $gl-btn-line-height; color: $gl-text-color-secondary; + white-space: nowrap; } .stat-link { + border-bottom: 0; + &:hover, &:focus { color: $gl-text-color; text-decoration: underline; + border-bottom: 0; } } @@ -868,7 +929,7 @@ pre.light-well { } .git-clone-holder { - width: 380px; + width: 320px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index e351dd7c0bb..5a594920e44 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -106,7 +106,7 @@ .settings-list-icon { color: $gl-text-color-secondary; - font-size: $settings-icon-size; + font-size: $default-icon-size; line-height: 42px; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e5b38898a67..7cd68d6b92a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base def append_info_to_payload(payload) super + payload[:ua] = request.env["HTTP_USER_AGENT"] payload[:remote_ip] = request.remote_ip logged_user = auth_user diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 22b39f47bf0..a2c96f5d635 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -1,5 +1,6 @@ module IssuableCollections extend ActiveSupport::Concern + include CookiesHelper include SortingHelper include Gitlab::IssuableMetadata include Gitlab::Utils::StrongMemoize @@ -107,11 +108,14 @@ module IssuableCollections end def set_sort_order_from_cookie - cookies[remember_sorting_key] = params[:sort] if params[:sort].present? + sort_param = params[:sort] if params[:sort].present? # fallback to legacy cookie value for backward compatibility - cookies[remember_sorting_key] ||= cookies['issuable_sort'] - cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key]) - params[:sort] = cookies[remember_sorting_key] + sort_param ||= cookies['issuable_sort'] + sort_param ||= cookies[remember_sorting_key] + + sort_value = update_cookie_value(sort_param) + set_secure_cookie(remember_sorting_key, sort_value) + params[:sort] = sort_value end def remember_sorting_key diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 237c93daee8..382ec91f771 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -1,7 +1,11 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') if attachment - redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } + # Response-Content-Type will not override an existing Content-Type in + # Google Cloud Storage, so the metadata needs to be cleared on GCS for + # this to work. However, this override works with AWS. + redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}", + "response-content-type" => guess_content_type(attachment) } # By default, Rails will send uploads with an extension of .js with a # content-type of text/javascript, which will trigger Rails' # cross-origin JavaScript protection. @@ -18,4 +22,14 @@ module SendFileUpload redirect_to file_upload.url(**redirect_params) end end + + def guess_content_type(filename) + types = MIME::Types.type_for(filename) + + if types.present? + types.first.content_type + else + "application/octet-stream" + end + end end diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 434459a225a..bb8c245a5b7 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -53,6 +53,8 @@ module UploadsActions maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i) render json: authorized + rescue SocketError + render json: "Error uploading file", status: :internal_server_error end private diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 863f50e8e66..3e0076ac935 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -11,7 +11,7 @@ class Groups::LabelsController < Groups::ApplicationController def index respond_to do |format| format.html do - @labels = @group.labels.page(params[:page]) + @labels = @available_labels.page(params[:page]) end format.json do render json: LabelSerializer.new.represent_appearance(@available_labels) @@ -113,7 +113,7 @@ class Groups::LabelsController < Groups::ApplicationController group_id: @group.id, only_group_labels: params[:only_group_labels], include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute + include_descendant_groups: params[:include_descendant_groups], + search: params[:search]).execute end end diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/instance_statistics/cohorts_controller.rb index 7eba0a5ecdd..4b4e39db2e1 100644 --- a/app/controllers/instance_statistics/cohorts_controller.rb +++ b/app/controllers/instance_statistics/cohorts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController + before_action :authenticate_usage_ping_enabled_or_admin! + def index if Gitlab::CurrentSettings.usage_ping_enabled cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do @@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon @cohorts = CohortsSerializer.new.represent(cohorts_results) end end + + def authenticate_usage_ping_enabled_or_admin! + render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin? + end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b4f814fd3a4..695ffd90a85 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,4 +1,5 @@ class Projects::ApplicationController < ApplicationController + include CookiesHelper include RoutableActions include ChecksCollaboration @@ -74,7 +75,7 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? + set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present? end def require_pages_enabled! diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 358fe59618b..b4fd09c06e5 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -157,7 +157,8 @@ class Projects::ClustersController < Projects::ApplicationController :namespace, :api_url, :token, - :ca_cert + :ca_cert, + :authorization_type ]).merge( provider_type: :user, platform_type: :kubernetes diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 2da2aad9b33..bbf8c7d5cbc 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -66,6 +66,7 @@ class Projects::HooksController < Projects::ApplicationController :enable_ssl_verification, :token, :url, + :push_events_branch_filter, *ProjectHook.triggers.values ) end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 48a09e1ddb8..0fed7f6576c 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -36,54 +36,47 @@ class Projects::RefsController < Projects::ApplicationController end def logs_tree - @offset = if params[:offset].present? - params[:offset].to_i - else - 0 - end + summary = ::Gitlab::TreeSummary.new( + @commit, + @project, + path: @path, + offset: params[:offset], + limit: 25 + ) - @limit = 25 - - @path = params[:path] - - contents = [] - contents.push(*tree.trees) - contents.push(*tree.blobs) - contents.push(*tree.submodules) - - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 - @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do - contents[@offset, @limit].to_a.map do |content| - file = @path ? File.join(@path, content.name) : content.name - last_commit = @repo.last_commit_for_path(@commit.id, file) - commit_path = project_commit_path(@project, last_commit) if last_commit - { - file_name: content.name, - commit: last_commit, - type: content.type, - commit_path: commit_path - } - end - end - - offset = (@offset + @limit) - if contents.size > offset - @more_log_url = logs_file_project_ref_path(@project, @ref, @path || '', offset: offset) - end + @logs, commits = summary.summarize + @more_log_url = more_url(summary.next_offset) if summary.more? respond_to do |format| format.html { render_404 } format.json do - response.headers["More-Logs-Url"] = @more_log_url - + response.headers["More-Logs-Url"] = @more_log_url if summary.more? render json: @logs end - format.js + + # The commit titles must be rendered and redacted before being shown. + # Doing it here allows us to apply performance optimizations that avoid + # N+1 problems + format.js do + prerender_commit_full_titles!(commits) + end end end private + def more_url(offset) + logs_file_project_ref_path(@project, @ref, @path, offset: offset) + end + + def prerender_commit_full_titles!(commits) + # Preload commit authors as they are used in rendering + commits.each(&:lazy_author) + + renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project) + renderer.render(commits, :full_title) + end + def validate_ref_id return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex end diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb new file mode 100644 index 00000000000..ea0251bffb6 --- /dev/null +++ b/app/finders/template_finder.rb @@ -0,0 +1,37 @@ +class TemplateFinder + VENDORED_TEMPLATES = { + dockerfiles: ::Gitlab::Template::DockerfileTemplate, + gitignores: ::Gitlab::Template::GitignoreTemplate, + gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate + }.freeze + + class << self + def build(type, params = {}) + if type == :licenses + LicenseTemplateFinder.new(params) + else + new(type, params) + end + end + end + + attr_reader :type, :params + + attr_reader :vendored_templates + private :vendored_templates + + def initialize(type, params = {}) + @type = type + @params = params + + @vendored_templates = VENDORED_TEMPLATES.fetch(type) + end + + def execute + if params[:name] + vendored_templates.find(params[:name]) + else + vendored_templates.all + end + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 00ebafd177b..96f7415ae98 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -158,32 +158,35 @@ module BlobHelper end def licenses_for_select - return @licenses_for_select if defined?(@licenses_for_select) - - grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) - categories = grouped_licenses.keys - - @licenses_for_select = categories.each_with_object({}) do |category, hash| - hash[category] = grouped_licenses[category].map do |license| - { name: license.name, id: license.id } - end - end + @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute) end def ref_project @ref_project ||= @target_project || @project end + def template_dropdown_names(items) + grouped = items.group_by(&:category) + categories = grouped.keys + + categories.each_with_object({}) do |category, hash| + hash[category] = grouped[category].map do |item| + { name: item.name, id: item.id } + end + end + end + private :template_dropdown_names + def gitignore_names - @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names + @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute) end def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) + @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls).execute) end def dockerfile_names - @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names + @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute) end def blob_editor_paths diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 26e3850a540..2b3fe57767c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -61,7 +61,7 @@ module ButtonHelper dropdown_description = http_dropdown_description(protocol) append_url = project.http_url_to_repo if append_link - dropdown_item_with_description(protocol, dropdown_description, href: append_url) + dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' }) end def http_dropdown_description(protocol) @@ -80,16 +80,17 @@ module ButtonHelper append_url = project.ssh_url_to_repo if append_link - dropdown_item_with_description('SSH', dropdown_description, href: append_url) + dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' }) end - def dropdown_item_with_description(title, description, href: nil) + def dropdown_item_with_description(title, description, href: nil, data: nil) button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description content_tag (href ? :a : :span), (href ? button_content : title), class: "#{title.downcase}-selector", - href: (href if href) + href: (href if href), + data: (data if data) end end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 8fd0b6f14c6..73049c74d80 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -11,4 +11,8 @@ module ClustersHelper render 'projects/clusters/gcp_signup_offer_banner' end end + + def rbac_clusters_feature_enabled? + Feature.enabled?(:rbac_clusters) + end end diff --git a/app/helpers/cookies_helper.rb b/app/helpers/cookies_helper.rb new file mode 100644 index 00000000000..3a7e9987190 --- /dev/null +++ b/app/helpers/cookies_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module CookiesHelper + def set_secure_cookie(key, value, httponly: false, permanent: false) + cookie_jar = permanent ? cookies.permanent : cookies + + cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly } + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index cb6f709c604..269acf5b2e2 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -110,10 +110,12 @@ module EventsHelper event.note_target) elsif event.note? if event.note_target - event_note_target_path(event) + event_note_target_url(event) end elsif event.push? push_event_feed_url(event) + elsif event.created_project? + project_url(event.project) end end @@ -145,14 +147,14 @@ module EventsHelper end end - def event_note_target_path(event) + def event_note_target_url(event) if event.commit_note? - project_commit_path(event.project, event.note_target, anchor: dom_id(event.target)) + project_commit_url(event.project, event.note_target, anchor: dom_id(event.target)) elsif event.project_snippet_note? - project_snippet_path(event.project, event.note_target, anchor: dom_id(event.target)) + project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target)) else - polymorphic_path([event.project.namespace.becomes(Namespace), - event.project, event.note_target], + polymorphic_url([event.project.namespace.becomes(Namespace), + event.project, event.note_target], anchor: dom_id(event.target)) end end @@ -166,7 +168,7 @@ module EventsHelper event.note_target_reference end - link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip') + link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip') else content_tag(:strong, '(deleted)') end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a8a10c98d69..a5612372aa6 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -86,7 +86,7 @@ module IconsHelper end end - def visibility_level_icon(level, fw: true) + def visibility_level_icon(level, fw: true, options: {}) name = case level when Gitlab::VisibilityLevel::PRIVATE @@ -99,7 +99,7 @@ module IconsHelper name << " fw" if fw - icon(name) + icon(name, options) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index cbb971cf8b7..3adaa1366c0 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -107,23 +107,23 @@ module MarkupHelper def markup(file_name, text, context = {}) context[:project] ||= @project - context[:markdown_engine] ||= :redcarpet + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = context.delete(:rendered) || markup_unsafe(file_name, text, context) prepare_for_rendering(html, context) end - def render_wiki_content(wiki_page) + def render_wiki_content(wiki_page, context = {}) text = wiki_page.content return '' unless text.present? - context = { + context.merge!( pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug, - issuable_state_filter_enabled: true, - markdown_engine: :redcarpet - } + issuable_state_filter_enabled: true + ) + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = case wiki_page.format @@ -178,6 +178,10 @@ module MarkupHelper end end + def commonmark_for_repositories_enabled? + Feature.enabled?(:commonmark_for_repositories, default_enabled: true) + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 18b3badda8d..80b45176a62 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -252,6 +252,10 @@ module ProjectsHelper "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" end + def legacy_render_context(params) + params[:legacy_render] ? { markdown_engine: :redcarpet } : {} + end + private def get_project_nav_tabs(project, current_user) @@ -351,6 +355,10 @@ module ProjectsHelper end end + def default_clone_label + _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase } + end + def default_clone_protocol if allowed_protocols_present? enabled_protocol diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index da5fe25c07d..657a3227dd4 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -1,6 +1,7 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze + CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -11,6 +12,10 @@ module UserCalloutsHelper !user_dismissed?(GCP_SIGNUP_OFFER) end + def show_cluster_security_warning? + !user_dismissed?(CLUSTER_SECURITY_WARNING) + end + private def user_dismissed?(feature_name) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index cf2fe5a2019..7b64869c9ea 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -138,7 +138,7 @@ module VisibilityLevelHelper end def project_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" + "#{project_visibility_level_description(level)}" end def visibility_level_label(level) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 41f9eedd4bd..17940aeb900 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -1,4 +1,6 @@ module WikiHelper + include API::Helpers::RelatedResourcesHelpers + # Produces a pure text breadcrumb for a given page. # # page_slug - The slug of a WikiPage object. @@ -39,4 +41,8 @@ module WikiHelper end end end + + def wiki_attachment_upload_url + expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) + end end diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb new file mode 100644 index 00000000000..9705a3052d4 --- /dev/null +++ b/app/mailers/emails/auto_devops.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Emails + module AutoDevops + def autodevops_disabled_email(pipeline, recipient) + @pipeline = pipeline + @project = pipeline.project + + add_project_headers + + mail(to: recipient, + subject: auto_devops_disabled_subject(@project.name)) do |format| + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } + end + end + + private + + def auto_devops_disabled_subject(project_name) + subject("Auto DevOps pipeline was disabled for #{project_name}") + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index f4eeb85270e..f7347ee61b4 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -12,6 +12,7 @@ class Notify < BaseMailer include Emails::Profile include Emails::Pipelines include Emails::Members + include Emails::AutoDevops helper MergeRequestsHelper helper DiffHelper diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index df470930e9e..c133f4e6dbb 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -125,6 +125,10 @@ class NotifyPreview < ActionMailer::Preview Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) end + def autodevops_disabled_email + Notify.autodevops_disabled_email(pipeline, user.email).message + end + private def project diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 17b7ee4f07e..93fc1b145b2 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -48,6 +48,20 @@ module Ci gzip: 3 } + # `file_location` indicates where actual files are stored. + # Ideally, actual files should be stored in the same directory, and use the same + # convention to generate its path. However, sometimes we can't do so due to backward-compatibility. + # + # legacy_path ... The actual file is stored at a path consists of a timestamp + # and raw project/model IDs. Those rows were migrated from + # `ci_builds.artifacts_file` and `ci_builds.artifacts_metadata` + # hashed_path ... The actual file is stored at a path consists of a SHA2 based on the project ID. + # This is the default value. + enum file_location: { + legacy_path: 1, + hashed_path: 2 + } + FILE_FORMAT_ADAPTERS = { gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter }.freeze @@ -72,6 +86,12 @@ module Ci [nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store) end + def hashed_path? + return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column + + super || self.file_location.nil? + end + def expire_in expire_at - Time.now if expire_at end @@ -108,7 +128,7 @@ module Ci end def update_project_statistics_after_destroy - update_project_statistics(-self.size) + update_project_statistics(-self.size.to_i) end def update_project_statistics(difference) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 526bf7af99b..2955e0b2bca 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -161,6 +161,12 @@ module Ci PipelineNotificationWorker.perform_async(pipeline.id) end end + + after_transition any => [:failed] do |pipeline| + next unless pipeline.auto_devops_source? + + pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) } + end end scope :internal, -> { where(source: internal_sources) } diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 55bbf7cae7e..423071ec024 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -32,7 +32,8 @@ module Clusters def install_command Gitlab::Kubernetes::Helm::InitCommand.new( name: name, - files: files + files: files, + rbac: cluster.platform_kubernetes_rbac? ) end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 93f654e0638..bd0286ee3f9 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -39,6 +39,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ef1c76c03bd..3d84eeed5a8 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -40,6 +40,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 88399dbbb95..46d0388a464 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -48,6 +48,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) @@ -71,7 +72,7 @@ module Clusters private def kube_client - cluster&.kubeclient + cluster&.kubeclient&.core_client end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index bde255723c8..a4a2e2b79a6 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -33,6 +33,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7cf75403ab6..d7011ef447a 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -42,6 +42,7 @@ module Clusters delegate :on_creation?, to: :provider, allow_nil: true delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true + delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e6ddca0d5d0..3a335909101 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -5,6 +5,7 @@ module Clusters class Kubernetes < ActiveRecord::Base include Gitlab::Kubernetes include ReactiveCaching + include EnumWithNil self.table_name = 'cluster_platforms_kubernetes' self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] } @@ -47,6 +48,12 @@ module Clusters alias_method :active?, :enabled? + enum_with_nil authorization_type: { + unknown_authorization: nil, + rbac: 1, + abac: 2 + } + def actual_namespace if namespace.present? namespace @@ -95,7 +102,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end private @@ -115,15 +122,16 @@ module Clusters slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token raise "Either username/password or token is required to access API" end - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -133,7 +141,7 @@ module Clusters # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -157,15 +165,6 @@ module Clusters { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/commit.rb b/app/models/commit.rb index 594972ad344..49c36ad9d3f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -22,6 +22,7 @@ class Commit attr_accessor :project, :author attr_accessor :redacted_description_html attr_accessor :redacted_title_html + attr_accessor :redacted_full_title_html attr_reader :gpg_commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index c0233661a9b..0d5311a9985 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -9,7 +9,7 @@ module Avatarable include Gitlab::Utils::StrongMemoize validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i }, if: :avatar_changed? mount_uploader :avatar, AvatarUploader diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 6e80365ee5b..c93b6589ee7 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -9,23 +9,46 @@ module CaseSensitivity # # Unlike other ActiveRecord methods this method only operates on a Hash. def iwhere(params) - criteria = self - cast_lower = Gitlab::Database.postgresql? + criteria = self params.each do |key, value| - column = ActiveRecord::Base.connection.quote_table_name(key) + criteria = case value + when Array + criteria.where(value_in(key, value)) + else + criteria.where(value_equal(key, value)) + end + end + + criteria + end - condition = - if cast_lower - "LOWER(#{column}) = LOWER(:value)" - else - "#{column} = :value" - end + private + + def value_equal(column, value) + lower_value = lower_value(value) + + lower_column(arel_table[column]).eq(lower_value).to_sql + end - criteria = criteria.where(condition, value: value) + def value_in(column, values) + lower_values = values.map do |value| + lower_value(value) end - criteria + lower_column(arel_table[column]).in(lower_values).to_sql + end + + def lower_value(value) + return value if Gitlab::Database.mysql? + + Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)]) + end + + def lower_column(column) + return column if Gitlab::Database.mysql? + + column.lower end end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f881ce2321c..7f14d78e976 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -49,7 +49,7 @@ module Issuable end end - has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index e62e680af6e..af387c99f3d 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -50,14 +50,20 @@ module ProtectedRef .map(&:"#{action}_access_levels").flatten end + # Returns all protected refs that match the given ref name. + # This checks all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_refs` to search + # through, to avoid calling out to the database. def matching(ref_name, protected_refs: nil) - ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + (protected_refs || self.all).select { |protected_ref| protected_ref.matches?(ref_name) } end end private def ref_matcher - @ref_matcher ||= ProtectedRefMatcher.new(self) + @ref_matcher ||= RefMatcher.new(self.name) end end diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index 223a61119e5..c52baa0524c 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -29,6 +29,12 @@ module TriggerableHooks public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend end + def select_active(hooks_scope, data) + select do |hook| + ActiveHookFilter.new(hook).matches?(hooks_scope, data) + end + end + private def triggerable_hooks(hooks) diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb new file mode 100644 index 00000000000..ea046bea368 --- /dev/null +++ b/app/models/hooks/active_hook_filter.rb @@ -0,0 +1,14 @@ +class ActiveHookFilter + def initialize(hook) + @hook = hook + @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter) + end + + def matches?(hooks_scope, data) + return true if hooks_scope != :push_hooks + return true if @hook.push_events_branch_filter.blank? + + branch_name = Gitlab::Git.branch_name(data[:ref]) + @push_events_filter_matcher.matches?(branch_name) + end +end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index f18aadefa5c..20f15c15277 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -9,6 +9,7 @@ class WebHook < ActiveRecord::Base allow_local_network: lambda(&:allow_local_requests?) } validates :token, format: { without: /\n/ } + validates :push_events_branch_filter, branch_filter: true def execute(data, hook_name) WebHookService.new(self, data, hook_name).execute diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 779657b25d5..1d93a55e8e9 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -3,7 +3,7 @@ class LabelLink < ActiveRecord::Base include Importable - belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations + belongs_to :target, polymorphic: true, inverse_of: :label_links # rubocop:disable Cop/PolymorphicAssociations belongs_to :label validates :target, presence: true, unless: :importing? diff --git a/app/models/project.rb b/app/models/project.rb index 435ecc59bc4..997c1cf98fc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1186,10 +1186,9 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) run_after_commit_or_now do - hooks.hooks_for(hooks_scope).each do |hook| + hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook| hook.async_execute(data, hooks_scope.to_s) end - SystemHooksService.new.execute_hooks(data, hooks_scope) end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index bda1f67b8ff..f119555f16b 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -96,10 +96,10 @@ class KubernetesService < DeploymentService # Check we can connect to the Kubernetes API def test(*args) - kubeclient = build_kubeclient! + kubeclient = build_kube_client! - kubeclient.discover - { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + kubeclient.core_client.discover + { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" } rescue => err { success: false, result: err } end @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end def deprecated? @@ -182,11 +182,12 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace && token - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -196,7 +197,7 @@ class KubernetesService < DeploymentService # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -220,15 +221,6 @@ class KubernetesService < DeploymentService { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb deleted file mode 100644 index bfa9180ac93..00000000000 --- a/app/models/protected_ref_matcher.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -class ProtectedRefMatcher - def initialize(protected_ref) - @protected_ref = protected_ref - end - - # Returns all protected refs that match the given ref name. - # This checks all records from the scope built up so far, and does - # _not_ return a relation. - # - # This method optionally takes in a list of `protected_refs` to search - # through, to avoid calling out to the database. - def self.matching(type, ref_name, protected_refs: nil) - (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) } - end - - # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) - # that match the current protected ref. - def matching(refs) - refs.select { |ref| @protected_ref.matches?(ref.name) } - end - - # Checks if the protected ref matches the given ref name. - def matches?(ref_name) - return false if @protected_ref.name.blank? - - exact_match?(ref_name) || wildcard_match?(ref_name) - end - - # Checks if this protected ref contains a wildcard - def wildcard? - @protected_ref.name && @protected_ref.name.include?('*') - end - - protected - - def exact_match?(ref_name) - @protected_ref.name == ref_name - end - - def wildcard_match?(ref_name) - return false unless wildcard? - - wildcard_regex === ref_name - end - - def wildcard_regex - @wildcard_regex ||= begin - name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE') - quoted_name = Regexp.quote(name) - regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') - /\A#{regex_string}\z/ - end - end -end diff --git a/app/models/ref_matcher.rb b/app/models/ref_matcher.rb new file mode 100644 index 00000000000..fa7d2c0f06c --- /dev/null +++ b/app/models/ref_matcher.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class RefMatcher + def initialize(ref_name_or_pattern) + @ref_name_or_pattern = ref_name_or_pattern + end + + # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) + # that match the current protected ref. + def matching(refs) + refs.select { |ref| matches?(ref.name) } + end + + # Checks if the protected ref matches the given ref name. + def matches?(ref_name) + return false if @ref_name_or_pattern.blank? + + exact_match?(ref_name) || wildcard_match?(ref_name) + end + + # Checks if this protected ref contains a wildcard + def wildcard? + @ref_name_or_pattern && @ref_name_or_pattern.include?('*') + end + + protected + + def exact_match?(ref_name) + @ref_name_or_pattern == ref_name + end + + def wildcard_match?(ref_name) + return false unless wildcard? + + wildcard_regex === ref_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = @ref_name_or_pattern.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index cf255c8951f..929d28b9d88 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -580,7 +580,12 @@ class Repository end def rendered_readme - MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme + return unless readme + + context = { project: project } + context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled? + + MarkupHelper.markup_unsafe(readme.name, readme.data, context) end cache_method :rendered_readme diff --git a/app/models/user.rb b/app/models/user.rb index f21ca1c569f..0fcc952b5cd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -257,6 +257,7 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + scope :by_username, -> (usernames) { iwhere(username: usernames) } # Limits the users to those that have TODOs, optionally in the given state. # @@ -444,11 +445,11 @@ class User < ActiveRecord::Base end def find_by_username(username) - iwhere(username: username).take + by_username(username).take end def find_by_username!(username) - iwhere(username: username).take! + by_username(username).take! end def find_by_personal_access_token(token_string) diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 97e955ace36..1cd05cf3aac 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -5,7 +5,8 @@ class UserCallout < ActiveRecord::Base enum feature_name: { gke_cluster_integration: 1, - gcp_signup_offer: 2 + gcp_signup_offer: 2, + cluster_security_warning: 3 } validates :user, presence: true diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index 198bb168d85..6d8b575102e 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy rule { assignee_or_author }.policy do enable :read_issue enable :update_issue + enable :reopen_issue enable :read_merge_request enable :update_merge_request end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index 94b5f37c682..a0706eaa46c 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy prevent :update_issue prevent :admin_issue end + + rule { locked }.policy do + prevent :reopen_issue + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index fd6cc504a3b..273a93a1423 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -180,6 +180,7 @@ class ProjectPolicy < BasePolicy enable :fork_project enable :create_project_snippet enable :update_issue + enable :reopen_issue enable :admin_issue enable :admin_label enable :admin_list diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 4c2f33213d6..6a54054badc 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated presents :project + AnchorData = Struct.new(:enabled, :label, :link, :class_modifier) + MAX_TAGS_TO_SHOW = 3 + def statistics_anchors(show_auto_devops_callout:) [ + readme_anchor_data, + changelog_anchor_data, + contribution_guide_anchor_data, files_anchor_data, commits_anchor_data, branches_anchor_data, tags_anchor_data, - readme_anchor_data, - changelog_anchor_data, - license_anchor_data, - contribution_guide_anchor_data, gitlab_ci_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data @@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ readme_anchor_data, changelog_anchor_data, - license_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, @@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_anchors [ + files_anchor_data, + commits_anchor_data, + branches_anchor_data, + tags_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.select { |item| item.enabled } @@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ new_file_anchor_data, readme_anchor_data, - license_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.reject { |item| item.enabled } @@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def files_anchor_data - OpenStruct.new(enabled: true, - label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, - link: project_tree_path(project)) + AnchorData.new(true, + _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, + empty_repo? ? nil : project_tree_path(project)) end def commits_anchor_data - OpenStruct.new(enabled: true, - label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, - link: project_commits_path(project, repository.root_ref)) + AnchorData.new(true, + n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, + empty_repo? ? nil : project_commits_path(project, repository.root_ref)) end def branches_anchor_data - OpenStruct.new(enabled: true, - label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, - link: project_branches_path(project)) + AnchorData.new(true, + n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, + empty_repo? ? nil : project_branches_path(project)) end def tags_anchor_data - OpenStruct.new(enabled: true, - label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, - link: project_tags_path(project)) + AnchorData.new(true, + n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, + empty_repo? ? nil : project_tags_path(project)) end def new_file_anchor_data if current_user && can_current_user_push_to_default_branch? - OpenStruct.new(enabled: false, - label: _('New file'), - link: project_new_blob_path(project, default_branch || 'master'), - class_modifier: 'new') + AnchorData.new(false, + _('New file'), + project_new_blob_path(project, default_branch || 'master'), + 'new') end end def readme_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.readme.nil? - OpenStruct.new(enabled: false, - label: _('Add Readme'), - link: add_readme_path) + AnchorData.new(false, + _('Add Readme'), + add_readme_path) elsif repository.readme - OpenStruct.new(enabled: true, - label: _('Readme'), - link: default_view != 'readme' ? readme_path : '#readme') + AnchorData.new(true, + _('Readme'), + default_view != 'readme' ? readme_path : '#readme') end end def changelog_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? - OpenStruct.new(enabled: false, - label: _('Add Changelog'), - link: add_changelog_path) + AnchorData.new(false, + _('Add Changelog'), + add_changelog_path) elsif repository.changelog.present? - OpenStruct.new(enabled: true, - label: _('Changelog'), - link: changelog_path) + AnchorData.new(true, + _('Changelog'), + changelog_path) end end def license_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? - OpenStruct.new(enabled: false, - label: _('Add License'), - link: add_license_path) - elsif repository.license_blob.present? - OpenStruct.new(enabled: true, - label: license_short_name, - link: license_path) + if repository.license_blob.present? + AnchorData.new(true, + license_short_name, + license_path) + else + if current_user && can_current_user_push_to_default_branch? + AnchorData.new(false, + _('Add license'), + add_license_path) + else + AnchorData.new(false, + _('No license. All rights reserved'), + nil) + end end end def contribution_guide_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? - OpenStruct.new(enabled: false, - label: _('Add Contribution guide'), - link: add_contribution_guide_path) + AnchorData.new(false, + _('Add Contribution guide'), + add_contribution_guide_path) elsif repository.contribution_guide.present? - OpenStruct.new(enabled: true, - label: _('Contribution guide'), - link: contribution_guide_path) + AnchorData.new(true, + _('Contribution guide'), + contribution_guide_path) end end def autodevops_anchor_data(show_auto_devops_callout: false) if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout - OpenStruct.new(enabled: auto_devops_enabled?, - label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), - link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + AnchorData.new(auto_devops_enabled?, + auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) elsif auto_devops_enabled? - OpenStruct.new(enabled: true, - label: _('Auto DevOps enabled'), - link: nil) + AnchorData.new(true, + _('Auto DevOps enabled'), + nil) end end @@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated cluster_link = new_project_cluster_path(project) end - OpenStruct.new(enabled: !clusters.empty?, - label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), - link: cluster_link) + AnchorData.new(!clusters.empty?, + clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), + cluster_link) end end def gitlab_ci_anchor_data if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? - OpenStruct.new(enabled: false, - label: _('Set up CI/CD'), - link: add_ci_yml_path) + AnchorData.new(false, + _('Set up CI/CD'), + add_ci_yml_path) elsif repository.gitlab_ci_yml.present? - OpenStruct.new(enabled: true, - label: _('CI/CD configuration'), - link: ci_configuration_path) + AnchorData.new(true, + _('CI/CD configuration'), + ci_configuration_path) end end def koding_anchor_data if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? - OpenStruct.new(enabled: false, - label: _('Set up Koding'), - link: add_koding_stack_path) + AnchorData.new(false, + _('Set up Koding'), + add_koding_stack_path) end end + def tags_to_show + project.tag_list.take(MAX_TAGS_TO_SHOW) + end + + def count_of_extra_tags_not_shown + if project.tag_list.count > MAX_TAGS_TO_SHOW + project.tag_list.count - MAX_TAGS_TO_SHOW + else + 0 + end + end + + def has_extra_tags? + count_of_extra_tags_not_shown > 0 + end + private def filename_path(filename) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 271ff668eda..b107fc26f18 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity erase_project_job_path(project, build) end + expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build| + terminal_project_job_path(project, build) + end + expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :iid do |build| build.merge_request.iid @@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity def project build.project end + + def can_create_build_terminal? + can?(current_user, :create_build_terminal, build) && build.has_terminal? + end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 025f093a428..fc7b236f7da 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -7,8 +7,8 @@ module Files def initialize(*args) super - @author_email = params[:author_email] - @author_name = params[:author_name] + @author_email = params[:author_email] || current_user&.email + @author_name = params[:author_name] || current_user&.name @commit_message = params[:commit_message] @last_commit_sha = params[:last_commit_sha] diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 7d60c65bb79..1259c2c2b3d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -129,28 +129,19 @@ class IssuableBaseService < BaseService params.merge!(command_params) end - def create_issuable(issuable, attributes, label_ids:) - issuable.with_transaction_returning_status do - if issuable.save - issuable.update(label_ids: label_ids) - end - end - end - def create(issuable) handle_quick_actions_on_create(issuable) filter_params(issuable) params.delete(:state_event) params[:author] ||= current_user - - label_ids = process_label_ids(params) + params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params) issuable.assign_attributes(params) before_create(issuable) - if params.present? && create_issuable(issuable, params, label_ids: label_ids) + if issuable.save after_create(issuable) execute_hooks(issuable) invalidate_cache_counts(issuable, users: issuable.assignees) diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 3bd53f9ccdc..56d59b235a7 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -3,7 +3,7 @@ module Issues class ReopenService < Issues::BaseService def execute(issue) - return issue unless can?(current_user, :update_issue, issue) + return issue unless can?(current_user, :reopen_issue, issue) if issue.reopen event_service.reopen_issue(issue, current_user) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 55750269bb4..0e76d2cc3ab 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -20,6 +20,8 @@ module MergeRequests if merge_request.can_be_created compare_branches assign_title_and_description + assign_labels + assign_milestone end merge_request @@ -135,6 +137,20 @@ module MergeRequests append_closes_description end + def assign_labels + return unless target_project.issues_enabled? && issue + return if merge_request.label_ids&.any? + + merge_request.label_ids = issue.try(:label_ids) + end + + def assign_milestone + return unless target_project.issues_enabled? && issue + return if merge_request.milestone_id.present? + + merge_request.milestone_id = issue.try(:milestone_id) + end + def append_closes_description return unless issue&.to_reference.present? @@ -185,7 +201,9 @@ module MergeRequests end def issue - @issue ||= target_project.get_issue(issue_iid, current_user) + strong_memoize(:issue) do + target_project.get_issue(issue_iid, current_user) + end end end end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index fd91dc4acd0..d9a29693987 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -16,8 +16,6 @@ module MergeRequests def execute return error('Invalid issue iid') unless @issue_iid.present? && issue.present? - params[:label_ids] = issue.label_ids if issue.label_ids.any? - result = CreateBranchService.new(project, current_user).execute(branch_name, ref) return result if result[:status] == :error @@ -58,8 +56,7 @@ module MergeRequests source_project_id: project.id, source_branch: branch_name, target_project_id: project.id, - target_branch: ref, - milestone_id: issue.milestone_id + target_branch: ref } end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4511c500fca..50fa373025b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -407,6 +407,12 @@ class NotificationService end end + def autodevops_disabled(pipeline, recipients) + recipients.each do |recipient| + mailer.autodevops_disabled_email(pipeline, recipient).deliver_later + end + end + def pages_domain_verification_succeeded(domain) recipients_for_pages_domain(domain).each do |user| mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 11b996ed4b6..de8757006f1 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService end def markdown_engine - CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + if params[:legacy_render] + :redcarpet + else + CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + end end end diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb new file mode 100644 index 00000000000..9745ab67dbd --- /dev/null +++ b/app/services/projects/auto_devops/disable_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Projects + module AutoDevops + class DisableService < BaseService + def execute + return false unless implicitly_enabled_and_first_pipeline_failure? + + disable_auto_devops + end + + private + + def implicitly_enabled_and_first_pipeline_failure? + project.has_auto_devops_implicitly_enabled? && + first_pipeline_failure? + end + + # We're using `limit` to optimize `auto_devops pipeline` query, + # since we only care about the first element, and using only `.count` + # is an expensive operation. See + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378 + # for more context. + def first_pipeline_failure? + auto_devops_pipelines.success.limit(1).count.zero? && + auto_devops_pipelines.failed.limit(1).count.nonzero? + end + + def disable_auto_devops + project.auto_devops_attributes = { enabled: false } + project.save! + end + + def auto_devops_pipelines + @auto_devops_pipelines ||= project.pipelines.auto_devops_source + end + end + end +end diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb new file mode 100644 index 00000000000..30fe0e371a6 --- /dev/null +++ b/app/services/wikis/create_attachment_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Wikis + class CreateAttachmentService < Files::CreateService + ATTACHMENT_PATH = 'uploads'.freeze + MAX_FILENAME_LENGTH = 255 + + delegate :wiki, to: :project + delegate :repository, to: :wiki + + def initialize(*args) + super + + @file_name = truncate_file_name(params[:file_name]) + @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name + @commit_message ||= "Upload attachment #{@file_name}" + @branch_name ||= wiki.default_branch + end + + def create_commit! + commit_result(create_transformed_commit(@file_content)) + end + + private + + def truncate_file_name(file_name) + return unless file_name.present? + return file_name if file_name.length <= MAX_FILENAME_LENGTH + + extension = File.extname(file_name) + truncate_at = MAX_FILENAME_LENGTH - extension.length - 1 + base_name = File.basename(file_name, extension)[0..truncate_at] + base_name + extension + end + + def validate! + validate_file_name! + validate_permissions! + end + + def validate_file_name! + raise_error('The file name cannot be empty') unless @file_name + end + + def validate_permissions! + unless can?(current_user, :create_wiki, project) + raise_error('You are not allowed to push to the wiki') + end + end + + def create_transformed_commit(content) + repository.create_file( + current_user, + @file_path, + content, + message: @commit_message, + branch_name: @branch_name, + author_email: @author_email, + author_name: @author_name) + end + + def commit_result(commit_id) + { + file_name: @file_name, + file_path: @file_path, + branch: @branch_name, + commit: commit_id + } + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index b1365659834..ffc1e5f75ca 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -122,12 +122,6 @@ class FileUploader < GitlabUploader } end - def markdown_link - markdown = +"[#{markdown_name}](#{secure_url})" - markdown.prepend("!") if image_or_video? || dangerous? - markdown - end - def to_h { alt: markdown_name, @@ -192,10 +186,6 @@ class FileUploader < GitlabUploader storage.delete_dir!(store_dir) # only remove when empty end - def markdown_name - (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") - end - def identifier @identifier ||= filename end diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index f6af023e0f9..557b13a8bd6 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -5,6 +5,7 @@ class JobArtifactUploader < GitlabUploader include ObjectStorage::Concern ObjectNotReadyError = Class.new(StandardError) + UnknownFileLocationError = Class.new(StandardError) storage_options Gitlab.config.artifacts @@ -23,10 +24,22 @@ class JobArtifactUploader < GitlabUploader def dynamic_segment raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id - creation_date = model.created_at.utc.strftime('%Y_%m_%d') + if model.hashed_path? + hashed_path + elsif model.legacy_path? + legacy_path + else + raise UnknownFileLocationError + end + end + def hashed_path File.join(disk_hash[0..1], disk_hash[2..3], disk_hash, - creation_date, model.job_id.to_s, model.id.to_s) + model.created_at.utc.strftime('%Y_%m_%d'), model.job_id.to_s, model.id.to_s) + end + + def legacy_path + File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.job_id.to_s) end def disk_hash diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 2a2b54a9270..e8a2dce7755 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -2,32 +2,7 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze - # We recommend using the .mp4 format over .mov. Videos in .mov format can - # still be used but you really need to make sure they are served with the - # proper MIME type video/mp4 and not video/quicktime or your videos won't play - # on IE >= 9. - # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html - VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze - # These extension types can contain dangerous code and should only be embedded inline with - # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". - DANGEROUS_EXT = %w[svg].freeze - - def image? - extension_match?(IMAGE_EXT) - end - - def video? - extension_match?(VIDEO_EXT) - end - - def image_or_video? - image? || video? - end - - def dangerous? - extension_match?(DANGEROUS_EXT) - end + include Gitlab::FileMarkdownLinkBuilder private diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb new file mode 100644 index 00000000000..ef482aaaa63 --- /dev/null +++ b/app/validators/branch_filter_validator.rb @@ -0,0 +1,35 @@ +# BranchFilterValidator +# +# Custom validator for branch names. Squishes whitespace and ignores empty +# string. This only checks that a string is a valid git branch name. It does +# not check whether a branch already exists. +# +# Example: +# +# class Webhook < ActiveRecord::Base +# validates :push_events_branch_filter, branch_name: true +# end +# +class BranchFilterValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value.squish! unless value.nil? + + if value.present? + value_without_wildcards = value.tr('*', 'x') + + unless Gitlab::GitRefValidator.validate(value_without_wildcards) + record.errors[attribute] << "is not a valid branch name" + end + + unless value.length <= 4000 + record.errors[attribute] << "is longer than the allowed length of 4000 characters." + end + end + end + + private + + def contains_wildcard?(value) + value.include?('*') + end +end diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 2495defb6a7..788595877ea 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -2,7 +2,7 @@ = form_errors(@application_setting) %fieldset - .form-group + .form-group.mb-2 .form-check = f.check_box :version_check_enabled, class: 'form-check-input' = f.label :version_check_enabled, class: 'form-check-label' do @@ -16,23 +16,26 @@ .form-check = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' = f.label :usage_ping_enabled, class: 'form-check-label' do - Enable usage ping + = _('Enable usage ping') .form-text.text-muted - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. + %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.') + + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + + %button.btn.js-usage-ping-payload-trigger{ type: 'button' } + .js-spinner.d-none= icon('spinner spin') + .js-text.d-inline= _('Preview payload') + %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') - .form-group + = _('The usage ping is disabled, and cannot be configured through this form.') + - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path } + = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe } + .form-group.mt-3 = f.label :instance_statistics_visibility_private, _('Instance Statistics visibility') = f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" - diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 6133a7646f4..194a8157013 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,6 +325,14 @@ .settings-content = render partial: 'repository_mirrors_form' += render_if_exists 'admin/application_settings/geo', expanded: expanded + += render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded + += render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded + += render_if_exists 'admin/application_settings/slack', expanded: expanded + = render_if_exists 'admin/application_settings/templates', expanded: expanded %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 0ee563ac066..17a9c8df872 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,9 +1,9 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login" + = f.label "Username or email", for: "user_login", class: 'label-bold' = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." .form-group - = f.label :password + = f.label :password, class: 'label-bold' = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 3723814debe..269a3721e06 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,14 +1,17 @@ -.omniauth-container - %p - %span.light - Sign in with - - providers = enabled_button_based_providers +.omniauth-container.prepend-top-15 + %label.label-bold.d-block + Sign in with + - providers = enabled_button_based_providers + .d-flex.justify-content-between.flex-wrap - providers.each do |provider| - %span.light - - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - %fieldset.prepend-top-10.remember-me - %label - = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + - has_icon = provider_has_icon?(provider) + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do + - if has_icon + = provider_image_tag(provider) %span - Remember me + = label_for_provider(provider) + %fieldset.remember-me + %label + = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + %span + Remember me diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index ee7369f54a9..90ed20404c5 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,24 +4,24 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name, 'Full name' + = f.label :name, 'Full name', class: 'label-bold' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group - = f.label :username + = f.label :username, class: 'label-bold' = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... .form-group - = f.label :email + = f.label :email, class: 'label-bold' = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." .form-group - = f.label :email_confirmation + = f.label :email_confirmation, class: 'label-bold' = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength - = f.label :password + = f.label :password, class: 'label-bold' = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." - %p.gl-field-hint Minimum length is #{@minimum_password_length} characters + %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group = check_box_tag :terms_opt_in, '1', false, required: true @@ -34,8 +34,3 @@ = recaptcha_tags .submit-container = f.submit "Register", class: "btn-register btn" -.clearfix.submit-container - %p - %span.light Didn't receive a confirmation email? - = succeed '.' do - = link_to "Request a new one", new_confirmation_path(:user) diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 4b6c4581eb3..6b8dd156874 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -4,7 +4,6 @@ -# Text diff discussions - expanded = local_assigns.fetch(:expanded, true) %tr.notes_holder{ class: ('hide' unless expanded) } - %td.notes_line{ colspan: 2 } - %td.notes_content + %td.notes_content{ colspan: 3 } .content{ class: ('hide' unless expanded) } = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true } diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index 079d9083dff..2e621c4082d 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -1,21 +1,17 @@ - expanded = [*discussions_left, *discussions_right].any?(&:expanded?) %tr.notes_holder{ class: ('hide' unless expanded) } - if discussions_left - %td.notes_line.old - %td.notes_content.parallel.old + %td.notes_content.parallel.old{ colspan: 2 } .content{ class: ('hide' unless discussions_left.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true } - else - %td.notes_line.old= ("") - %td.notes_content.parallel.old + %td.notes_content.parallel.old{ colspan: 2 } .content - if discussions_right - %td.notes_line.new - %td.notes_content.parallel.new + %td.notes_content.parallel.new{ colspan: 2 } .content{ class: ('hide' unless discussions_right.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true } - else - %td.notes_line.new= ("") - %td.notes_content.parallel.new + %td.notes_content.parallel.new{ colspan: 2 } .content diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index bc1d32607e4..c5b033b1185 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,7 +1,7 @@ %div{ xmlns: "http://www.w3.org/1999/xhtml" } %p %strong= event.author_name - = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id) + = link_to "(#{truncate_sha(event.commit_id)})", event_feed_url(event) %i at = event.created_at.to_s(:short) diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index e1e38a7e82f..e6821009d03 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,28 +1,38 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @group) -- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] +- search = params[:search] - if can_admin_label - content_for(:header_content) do .nav-controls = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" -- if @labels.exists? +- if @labels.exists? || search.present? #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } + .nav-controls + = form_tag group_labels_path(@group), method: :get do + .input-group + = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false } + %span.input-group-append + %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } + = icon("search") .labels-container.prepend-top-5 - .other-labels - - if can_admin_label - %h5{ class: ('hide' if hide) } Labels - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } - = paginate @labels, theme: 'gitlab' + - if @labels.any? + .other-labels + %h5= _('Labels') + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } + = paginate @labels, theme: 'gitlab' + - elsif search.present? + .nothing-here-block + = _('No labels with such name or description') - else = render 'shared/empty_states/labels' diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml index 5e9a8c083af..e135bab10d8 100644 --- a/app/views/instance_statistics/cohorts/index.html.haml +++ b/app/views/instance_statistics/cohorts/index.html.haml @@ -1,16 +1,16 @@ -- breadcrumb_title "Cohorts" +- breadcrumb_title _("Cohorts") - @no_container = true %div{ class: container_class } - if @cohorts = render 'cohorts_table' - = render 'usage_ping' - else .bs-callout.bs-callout-warning.clearfix %p - User cohorts are only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank' - is enabled. To enable it and see user cohorts, - visit - = succeed '.' do - = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics') + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics') + - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path } + = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe } diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml index 0a741b50960..0a5717f75e1 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml @@ -1,9 +1,14 @@ .container.convdev-empty .col-sm-12.justify-content-center.text-center = custom_icon('convdev_no_index') - %h4 Usage ping is not enabled - %p - ConvDev is only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' - is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective - = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' + %h4= _('Usage ping is not enabled') + - if !current_user.admin? + %p + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + %p + = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.') + - if current_user.admin? + = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml index dd63b98376f..1e7db4982d6 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/conversational_development_index/index.html.haml @@ -1,12 +1,13 @@ - @no_container = true -- page_title 'ConvDev Index' +- page_title _('ConvDev Index') +- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled .container - - if show_callout?('convdev_intro_callout_dismissed') + - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed') = render 'callout' .prepend-top-default - - if !Gitlab::CurrentSettings.usage_ping_enabled + - if !usage_ping_enabled = render 'disabled' - elsif @metric.blank? = render 'no_data' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0ca34b276a7..1f4d24d996c 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,7 @@ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' - = render "layouts/header/default" + = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index 95db8313821..e29f646ed4f 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -3,7 +3,7 @@ = render "layouts/head" %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' - = render "layouts/header/default" + = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index e8d31992149..044b49c12cc 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,3 +1,10 @@ +- if project + - search_path_url = search_path(project_id: project.id) +- elsif group + - search_path_url = search_path(group_id: group.id) +- else + - search_path_url = search_path + %header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid @@ -24,26 +31,25 @@ %li.nav-item.d-none.d-sm-none.d-md-block.m-auto = render 'layouts/search' unless current_controller?(:search) %li.nav-item.d-inline-block.d-sm-none.d-md-none - = link_to search_path, title: _('Search'), aria: { label: _("Search") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to search_path_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('search', size: 16) - - if header_link?(:issues) = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do - = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _("Issues") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('issues', size: 16) - issues_count = assigned_issuables_count(:issues) %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - if header_link?(:merge_requests) = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do - = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _("Merge requests") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('git-merge', size: 16) - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do - = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _("Todos") }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) @@ -56,7 +62,7 @@ = render 'layouts/header/current_user_dropdown' - if header_link?(:admin_impersonation) %li.nav-item.impersonation - = 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 + = 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) %li.nav-item @@ -64,8 +70,7 @@ - sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in') = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' - %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } - %span.sr-only= _("Toggle navigation") + %span.sr-only= _('Toggle navigation') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml index b8ff448f261..57180f27146 100644 --- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml +++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml @@ -18,16 +18,17 @@ %strong.fly-out-top-item-name = _('ConvDev Index') - = nav_link(controller: :cohorts) do - = link_to instance_statistics_cohorts_path do - .nav-icon-container - = sprite_icon('users') - %span.nav-item-name - = _('Cohorts') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do - = link_to instance_statistics_cohorts_path do - %strong.fly-out-top-item-name - = _('Cohorts') + - if Gitlab::CurrentSettings.usage_ping_enabled + = nav_link(controller: :cohorts) do + = link_to instance_statistics_cohorts_path do + .nav-icon-container + = sprite_icon('users') + %span.nav-item-name + = _('Cohorts') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do + = link_to instance_statistics_cohorts_path do + %strong.fly-out-top-item-name + = _('Cohorts') = render 'shared/sidebar_toggle_button' diff --git a/app/views/notify/_failed_builds.html.haml b/app/views/notify/_failed_builds.html.haml new file mode 100644 index 00000000000..7c563bb016c --- /dev/null +++ b/app/views/notify/_failed_builds.html.haml @@ -0,0 +1,32 @@ +%tr + %td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" } + had + = failed.size + failed + #{'build'.pluralize(failed.size)}. +%tr.table-warning + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" } + Logs may contain sensitive data. Please consider before forwarding this email. +%tr.section + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" } + %tbody + - failed.each do |build| + %tr.build-state + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" } + %tbody + %tr + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" } + %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/ + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" } + = build.stage + %td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" } + = render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build + %tr.build-log + - if build.has_trace? + %td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" } + %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" } + = build.trace.html(last_lines: 10).html_safe + - else + %td{ colspan: "2" } diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml new file mode 100644 index 00000000000..65a2f75a3e2 --- /dev/null +++ b/app/views/notify/autodevops_disabled_email.html.haml @@ -0,0 +1,49 @@ +%tr.alert + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" } + Auto DevOps pipeline was disabled for #{@project.name} + +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" } + The Auto DevOps pipeline failed for pipeline + %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" } + = "\##{@pipeline.iid}" + and has been disabled for + %a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" } + = @project.name + "." + In order to use the Auto DevOps pipeline with your project, please review the + %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages, + adjust your project accordingly, and turn on the Auto DevOps pipeline within your + %a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" } + CI/CD project settings. + +%tr.pre-section + %td{ style: 'text-align: center;border-bottom:1px solid #ededed' } + %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' } + Learn more about Auto DevOps + +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" } + = "\##{@pipeline.id}" + triggered by + - if @pipeline.user + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" } + %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/ + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" } + %a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" } + = @pipeline.user.name + - else + %td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" } + API + += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb new file mode 100644 index 00000000000..695780c3145 --- /dev/null +++ b/app/views/notify/autodevops_disabled_email.text.erb @@ -0,0 +1,20 @@ +Auto DevOps pipeline was disabled for <%= @project.name %> + +The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>). + +<% if @pipeline.user -%> + Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +<% else -%> + Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API +<% end -%> +<% failed = @pipeline.statuses.latest.failed -%> +had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. + +<% failed.each do |build| -%> + <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %> + Stage: <%= build.stage %> + Name: <%= build.name %> + <% if build.has_trace? -%> + Trace: <%= build.trace.raw(last_lines: 10) %> + <% end -%> +<% end -%> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index baafaa6e3a0..86dcca4a447 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -107,36 +107,5 @@ - else %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } API -- failed = @pipeline.statuses.latest.failed -%tr - %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } - had - = failed.size - failed - #{'build'.pluralize(failed.size)}. -%tr.table-warning - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } - Logs may contain sensitive data. Please consider before forwarding this email. -%tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } - %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } - %tbody - - failed.each do |build| - %tr.build-state - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" } - %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } - = build.stage - %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build - %tr.build-log - - if build.has_trace? - %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } - %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } - = build.trace.html(last_lines: 10).html_safe - - else - %td{ colspan: "2" } + += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 6f08a294c5d..9f79feb4ddd 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -77,7 +77,7 @@ = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." - else - = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." + = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - if @user.read_only_attribute?(:email) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1b6c4193c4d..ced6a2a0399 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,16 +1,35 @@ - empty_repo = @project.empty_repo? -.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } +- license = @project.license_anchor_data +.project-home-panel{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } - .avatar-container.s70.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70) - %h1.project-title.qa-project-name - = @project.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } - = visibility_level_icon(@project.visibility_level, fw: false) + .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8 + .project-title-row.d-flex.align-items-center + .avatar-container.project-avatar.float-none + = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile') + %h1.project-title.d-flex.align-items-baseline.qa-project-name + = @project.name + .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline + .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } + = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) + = visibility_level_label(@project.visibility_level) + - if license.present? + .project-license.d-inline-flex.align-items-baseline + = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link' + - if @project.tag_list.present? + .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil } + = sprite_icon('tag', size: 16, css_class: 'icon') + = @project.tags_to_show + - if @project.has_extra_tags? + = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } .project-home-desc - if @project.description.present? - = markdown_field(@project, :description) + .project-description + .project-description-markdown.read-more-container + = markdown_field(@project, :description) + %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" } + = _("Read more") + - if can?(current_user, :read_project, @project) .text-secondary.prepend-top-8 = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -25,34 +44,42 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } - .project-badges.prepend-top-default.append-bottom-default - - @project.badges.each do |badge| - %a.append-right-8{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: '' }> - - .project-repo-buttons - .count-buttons + - if @project.badges.present? + .project-badges.prepend-top-default.append-bottom-default + - @project.badges.each do |badge| + %a.append-right-8{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> + + .project-repo-buttons.d-inline-flex.flex-wrap + .count-buttons.d-inline-flex = render 'projects/buttons/star' = render 'projects/buttons/fork' - %span.d-none.d-sm-inline - - if can?(current_user, :download_code, @project) - .project-clone-holder - = render "shared/clone_panel" + - if can?(current_user, :download_code, @project) + .project-clone-holder.d-inline-flex.d-sm-none + = render "shared/mobile_clone_panel" - - if show_xcode_link?(@project) - .project-action-button.project-xcode.inline - = render "projects/buttons/xcode_link" + .project-clone-holder.d-none.d-sm-inline-flex + = render "shared/clone_panel" - - if current_user - - if can?(current_user, :download_code, @project) + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + + - if current_user + - if can?(current_user, :download_code, @project) + .d-none.d-sm-inline-flex = render 'projects/buttons/download', project: @project, ref: @ref + .d-none.d-sm-inline-flex = render 'projects/buttons/dropdown' + .d-none.d-sm-inline-flex = render 'projects/buttons/koding' + .d-none.d-sm-inline-flex = render 'shared/notifications/button', notification_setting: @notification_setting + .d-none.d-sm-inline-flex = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 15ec58289e3..4cf49f3cf62 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -1,7 +1,7 @@ - anchors = local_assigns.fetch(:anchors, []) - return unless anchors.any? -%ul.nav.justify-content-center +%ul.nav - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 5646dc464f8..5adca007f7e 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -2,7 +2,7 @@ %div{ class: container_class } .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@wiki_home) + = render_wiki_content(@wiki_home, legacy_render_context(params)) - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index a4b1b496b69..cf273aab108 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -5,6 +5,7 @@ %ul.blob-commit-info = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref + = render_if_exists 'projects/blob/owners', blob: blob = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 27cf040da7c..fdab8a53b41 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -21,7 +21,7 @@ Write %li - = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do + = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do = editing_preview_title(@blob.name) = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index da2cef17e8a..eb65cd90ea8 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -2,7 +2,7 @@ .diff-content - if markup?(@blob.name) .file-content.wiki - = markup(@blob.name, @content) + = markup(@blob.name, @content, legacy_render_context(params)) - else .file-content.code.js-syntax-highlight - unless @diff_lines.empty? diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index 230305b488d..bd12cadf240 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,4 +1,6 @@ - blob = viewer.blob -- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup) +- context = legacy_render_context(params) +- unless context[:markdown_engine] == :redcarpet + - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup) .file-content.wiki - = markup(blob.name, blob.data, rendered: rendered_markup) + = markup(blob.name, blob.data, context) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f880556a9f7..8da27ca7cb3 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,17 +1,17 @@ - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do - = custom_icon('icon_fork') - %span= s_('GoToYourFork|Fork') - - else - - can_create_fork = current_user.can?(:create_fork) - = link_to new_project_fork_path(@project), - class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}", - title: (_('You have reached your project limit') unless can_create_fork) do - = custom_icon('icon_fork') - %span= s_('CreateNewFork|Fork') - .count-with-arrow - %span.arrow - = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do - = @project.forks_count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.fork-count.count-badge-count.d-flex.align-items-center + = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do + = @project.forks_count + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') + - else + - can_create_fork = current_user.can?(:create_fork) + = link_to new_project_fork_path(@project), + class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index a2dc2730ecc..0d04ecb3a58 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,21 +1,19 @@ - if current_user - %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }> - - if current_user.starred?(@project) - = sprite_icon('star') - %span.starred= _('Unstar') - - else - = sprite_icon('star-o') - %span= s_('StarProject|Star') - .count-with-arrow - %span.arrow - %span.count.star-count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + %button.count-badge-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 has-tooltip star-btn', title: _('You must sign in to star a project') do - = sprite_icon('star') - #{ s_('StarProject|Star') } - .count-with-arrow - %span.arrow - %span.count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + = 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/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index f18caa3f4ac..73cfea0ef92 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,14 +1,15 @@ -%h4= s_('ClusterIntegration|Kubernetes cluster integration') +.hidden.js-cluster-error.bs-callout.bs-callout-danger{ role: 'alert' } + = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine') + %p.js-error-reason -.settings-content - .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine') - %p.js-error-reason +.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' } + = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') - .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') +.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } + = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } - = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - - %p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab') +- if show_cluster_security_warning? + .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning + %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } × + = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") + = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index b46b45fea49..d0a553e3414 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -2,14 +2,6 @@ = form_errors(@cluster) .form-group %h5= s_('ClusterIntegration|Integration status') - %p - - if @cluster.enabled? - - if can?(current_user, :update_cluster, @cluster) - = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.') - - else - = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.') - - else - = s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.') %label.append-bottom-0.js-cluster-enable-toggle-area %button{ type: 'button', class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", @@ -19,14 +11,13 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - if has_multiple_clusters?(@project) .form-group %h5= s_('ClusterIntegration|Environment scope') - %p - = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.") - = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') + = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') + .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") - if can?(current_user, :update_cluster, @cluster) .form-group @@ -38,8 +29,3 @@ %code * is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope') - - %h5= s_('ClusterIntegration|Security') - %p - = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.") - = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 08d2deff6f8..eddd3613c5f 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -23,7 +23,8 @@ .js-cluster-application-notice .flash-container - %section.settings.no-animate.expanded#cluster-integration + %section#cluster-integration + %h4= @cluster.name = render 'banner' = render 'integration_form' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index e8ef0008802..1f81e024ab9 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -25,5 +25,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 20a07d6695e..56b597d295a 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -26,5 +26,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml new file mode 100644 index 00000000000..ff6a9d49a61 --- /dev/null +++ b/app/views/projects/default_branch/_show.html.haml @@ -0,0 +1,21 @@ +- expanded = Rails.env.test? + +%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Default Branch') + %button.btn.js-settings-toggle + = expanded ? _('Collapse') : _('Expand') + %p + = _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.') + + .settings-content + - if @project.empty_repo? + .text-secondary + = _('A default branch cannot be chosen for an empty project.') + - else + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f| + %fieldset + .form-group + = f.label :default_branch, "Default Branch", class: 'label-bold' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fb837b27207..acdde9e0f70 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -36,11 +36,6 @@ = render_if_exists 'projects/classification_policy_settings', f: f - - unless @project.empty_repo? - .form-group - = f.label :default_branch, "Default Branch", class: 'label-bold' - = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project .form-group diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d47dc3d8143..d104608b2fe 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -32,9 +32,13 @@ = _('Otherwise it is recommended you start with one of the options below.') .prepend-top-20 -%nav.project-stats{ class: container_class } - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons +%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } @@ -42,7 +46,7 @@ .empty_wrapper %h3#repo-command-line-instructions.page-title-empty Command line instructions - .git-empty + .git-empty.js-git-empty %fieldset %h5 Git global setup %pre.bg-light @@ -54,7 +58,7 @@ %h5 Create a new repository %pre.bg-light :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} cd #{h @project.path} touch README.md git add README.md @@ -69,7 +73,7 @@ :preserve cd existing_folder git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} git add . git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? @@ -82,7 +86,7 @@ :preserve cd existing_repo git remote rename origin old-origin - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} - if @project.can_current_user_push_to_default_branch? %span>< git push -u origin --all diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index 60fe442014f..9a081a42b6f 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,4 +1,5 @@ -- breadcrumb_title "Issues" +- add_to_breadcrumbs "Issues", project_issues_path(@project) +- breadcrumb_title "New" - page_title "New Issue" %h3.page-title diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 2d036bd4e3e..b81d1a188f0 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -6,6 +6,7 @@ - page_card_attributes @issue.card_attributes - can_update_issue = can?(current_user, :update_issue, @issue) +- can_reopen_issue = can?(current_user, :reopen_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_create_issue = show_new_issue_link?(@project) @@ -40,6 +41,7 @@ %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' + - if can_reopen_issue %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - if can_report_spam %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' @@ -48,7 +50,7 @@ %li.divider %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' - = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue + = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue - if can_report_spam = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam' diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index a58179091ae..1bf42ded97a 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -39,4 +39,4 @@ - if can_update_merge_request = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit" - = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request + = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_merge_request diff --git a/app/views/projects/merge_requests/creations/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml index 3220512d60d..0f618826305 100644 --- a/app/views/projects/merge_requests/creations/new.html.haml +++ b/app/views/projects/merge_requests/creations/new.html.haml @@ -1,4 +1,5 @@ -- breadcrumb_title "Merge Requests" +- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project) +- breadcrumb_title "New" - page_title "New Merge Request" - if @merge_request.can_be_created && !params[:change_branches] diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 98c609d7bd4..a0bcaaf3c54 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -2,6 +2,7 @@ - page_title _("Repository") - @content_class = "limit-container-width" unless fluid_layout += render "projects/default_branch/show" = render "projects/mirrors/show" -# Protected branches & tags use a lot of nested partials. diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index df8a5742450..aba289c790f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -19,8 +19,13 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) - = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index abb3e918e87..406dccb74fb 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link" + = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index de692466fe5..7fb80450161 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,13 @@ - commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") - commit_message = commit_message % { page_title: @page.title } +- if params[:legacy_render] || !commonmark_for_repositories_enabled? + - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION +- else + - markdown_version = 0 = 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' }, - data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f| + data: { markdown_version: markdown_version } do |f| = form_errors(@page) - if @page.persisted? diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 28353927135..02c5a6ea55c 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,7 +12,7 @@ .blocks-container .block.block-first - if @sidebar_page - = render_wiki_content(@sidebar_page) + = render_wiki_content(@sidebar_page, legacy_render_context(params)) - else %ul.wiki-pages = render @sidebar_wiki_entries, context: 'sidebar' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index d80d2957466..71359708022 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -41,3 +41,8 @@ = render 'sidebar' #delete-wiki-modal.modal.fade + +- content_for :scripts_body do + -# haml-lint:disable InlineJavaScript + :javascript + window.uploads_path = "#{wiki_attachment_upload_url}"; diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index a08973c7f32..19b9744b508 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -26,6 +26,6 @@ .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@page) + = render_wiki_content(@page, legacy_render_context(params)) = render 'sidebar' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 57a0b64bfd5..8b95bdf9747 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -21,7 +21,7 @@ .file-content.wiki - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = markup(snippet.file_name, chunk[:data]) + = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3655c2a1d42..a2df0347fd6 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,14 +1,14 @@ - project = project || @project -.git-clone-holder.input-group +.git-clone-holder.js-git-clone-holder.input-group .input-group-prepend - if allowed_protocols_present? .input-group-text.clone-dropdown-btn.btn - %span + %span.js-clone-dropdown-label = enabled_project_button(project, enabled_protocol) - else %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } - %span + %span.js-clone-dropdown-label = default_clone_protocol.upcase = icon('caret-down') %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml new file mode 100644 index 00000000000..998985cabe1 --- /dev/null +++ b/app/views/shared/_mobile_clone_panel.html.haml @@ -0,0 +1,13 @@ +- project = project || @project +- ssh_copy_label = _("Copy SSH clone URL") +- http_copy_label = _("Copy HTTPS clone URL") + +.btn-group.mobile-git-clone.js-mobile-git-clone + = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default") + %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } } + = icon("caret-down", class: "dropdown-btn-icon") + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } } + %li + = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }) + %li + = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' }) diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml index 13bb4baee3f..a9c78547eae 100644 --- a/app/views/shared/groups/_empty_state.html.haml +++ b/app/views/shared/groups/_empty_state.html.haml @@ -1,4 +1,4 @@ -.groups-empty-state +.groups-empty-state.qa-groups-empty-state = custom_icon("icon_empty_groups") .text-content diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 933d4b2ea65..70e05eb1c8c 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -2,13 +2,15 @@ - display_issuable_type = issuable_display_type(issuable) - button_method = issuable_close_reopen_button_method(issuable) -- if can_update && is_current_user - = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" - = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" -- elsif can_update && !is_current_user - = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable +- if can_update + - if is_current_user + = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, + class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" + - else + = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable + - if can_reopen && is_current_user + = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, + class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - else = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 07ebb8680d2..9c5b9593bba 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -17,6 +17,7 @@ %strong Push events %p.light.ml-1 This URL will be triggered by a push to the repository + = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' %li = form.check_box :tag_push_events, class: 'form-check-input' = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 6b1d75c6e72..2d4656e8608 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -18,7 +18,7 @@ = event_action_name(event) %strong - if event.note? - = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title + = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title - elsif event.target = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f95df7ecf03..ae9dc8d4857 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1,4 +1,6 @@ --- +- auto_devops:auto_devops_disable + - cronjob:admin_email - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb new file mode 100644 index 00000000000..73ddc591505 --- /dev/null +++ b/app/workers/auto_devops/disable_worker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module AutoDevops + class DisableWorker + include ApplicationWorker + include AutoDevopsQueue + + def perform(pipeline_id) + pipeline = Ci::Pipeline.find(pipeline_id) + project = pipeline.project + + send_notification_email(pipeline, project) if disable_service(project).execute + end + + private + + def disable_service(project) + Projects::AutoDevops::DisableService.new(project) + end + + def send_notification_email(pipeline, project) + recipients = email_receivers_for(pipeline, project) + + return unless recipients.any? + + NotificationService.new.autodevops_disabled(pipeline, recipients) + end + + def email_receivers_for(pipeline, project) + recipients = [pipeline.user&.email] + recipients << project.owner.email unless project.group + recipients.uniq.compact + end + end +end diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb new file mode 100644 index 00000000000..aba928ccaab --- /dev/null +++ b/app/workers/concerns/auto_devops_queue.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +# +module AutoDevopsQueue + extend ActiveSupport::Concern + + included do + queue_namespace :auto_devops + end +end |