diff options
Diffstat (limited to 'app')
36 files changed, 452 insertions, 16 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 6de9ab9efb3..76f3020c5c2 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -45,6 +45,7 @@ const Api = { mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', adminStatisticsPath: '/api/:version/application/statistics', pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id', + lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -457,6 +458,14 @@ const Api = { return axios.get(url); }, + lsifData(projectPath, commitId, path) { + const url = Api.buildUrl(this.lsifPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':commit_id', commitId); + + return axios.get(url, { params: { path } }); + }, + buildUrl(url) { return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); }, diff --git a/app/assets/javascripts/boards/components/issue_count.vue b/app/assets/javascripts/boards/components/issue_count.vue index c50a3c1c0d3..d55f7151d7e 100644 --- a/app/assets/javascripts/boards/components/issue_count.vue +++ b/app/assets/javascripts/boards/components/issue_count.vue @@ -25,7 +25,7 @@ export default { </script> <template> - <div class="issue-count"> + <div class="issue-count text-nowrap"> <span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }"> {{ issuesSize }} </span> diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue new file mode 100644 index 00000000000..0e5f1f0485d --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/app.vue @@ -0,0 +1,43 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import Popover from './popover.vue'; + +export default { + components: { + Popover, + }, + computed: { + ...mapState(['currentDefinition', 'currentDefinitionPosition']), + }, + mounted() { + this.blobViewer = document.querySelector('.blob-viewer'); + + this.addGlobalEventListeners(); + this.fetchData(); + }, + beforeDestroy() { + this.removeGlobalEventListeners(); + }, + methods: { + ...mapActions(['fetchData', 'showDefinition']), + addGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.addEventListener('click', this.showDefinition); + } + }, + removeGlobalEventListeners() { + if (this.blobViewer) { + this.blobViewer.removeEventListener('click', this.showDefinition); + } + }, + }, +}; +</script> + +<template> + <popover + v-if="currentDefinition" + :position="currentDefinitionPosition" + :data="currentDefinition" + /> +</template> diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue new file mode 100644 index 00000000000..d5bbe430fcd --- /dev/null +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -0,0 +1,76 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + props: { + position: { + type: Object, + required: true, + }, + data: { + type: Object, + required: true, + }, + }, + data() { + return { + offsetLeft: 0, + }; + }, + computed: { + positionStyles() { + return { + left: `${this.position.x - this.offsetLeft}px`, + top: `${this.position.y + this.position.height}px`, + }; + }, + }, + watch: { + position: { + handler() { + this.$nextTick(() => this.updateOffsetLeft()); + }, + deep: true, + immediate: true, + }, + }, + methods: { + updateOffsetLeft() { + this.offsetLeft = Math.max( + 0, + this.$el.offsetLeft + this.$el.offsetWidth - window.innerWidth + 20, + ); + }, + }, + colorScheme: gon?.user_color_scheme, +}; +</script> + +<template> + <div + :style="positionStyles" + class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" + > + <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> + <div v-for="(hover, index) in data.hover" :key="index" class="border-bottom"> + <pre + v-if="hover.language" + ref="code-output" + :class="$options.colorScheme" + class="border-0 bg-transparent m-0 code highlight" + v-html="hover.value" + ></pre> + <p v-else ref="doc-output" class="p-3 m-0"> + {{ hover.value }} + </p> + </div> + <div v-if="data.definition_url" class="popover-body"> + <gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default"> + {{ __('Go to definition') }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js new file mode 100644 index 00000000000..2222c986dfe --- /dev/null +++ b/app/assets/javascripts/code_navigation/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import store from './store'; +import App from './components/app.vue'; + +Vue.use(Vuex); + +export default () => { + const el = document.getElementById('js-code-navigation'); + + store.dispatch('setInitialData', el.dataset); + + return new Vue({ + el, + store, + render(h) { + return h(App); + }, + }); +}; diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js new file mode 100644 index 00000000000..10483abfb23 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -0,0 +1,62 @@ +import api from '~/api'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import * as types from './mutation_types'; +import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils'; + +export default { + setInitialData({ commit }, data) { + commit(types.SET_INITIAL_DATA, data); + }, + requestDataError({ commit }) { + commit(types.REQUEST_DATA_ERROR); + createFlash(__('An error occurred loading code navigation')); + }, + fetchData({ commit, dispatch, state }) { + commit(types.REQUEST_DATA); + + api + .lsifData(state.projectPath, state.commitId, state.path) + .then(({ data }) => { + const normalizedData = data.reduce((acc, d) => { + if (d.hover) { + acc[`${d.start_line}:${d.start_char}`] = d; + addInteractionClass(d); + } + return acc; + }, {}); + + commit(types.REQUEST_DATA_SUCCESS, normalizedData); + }) + .catch(() => dispatch('requestDataError')); + }, + showDefinition({ commit, state }, { target: el }) { + let definition; + let position; + + if (!state.data) return; + + const isCurrentElementPopoverOpen = el.classList.contains('hll'); + + if (getCurrentHoverElement()) { + getCurrentHoverElement().classList.remove('hll'); + } + + if (el.classList.contains('js-code-navigation') && !isCurrentElementPopoverOpen) { + const { lineIndex, charIndex } = el.dataset; + + position = { + x: el.offsetLeft, + y: el.offsetTop, + height: el.offsetHeight, + }; + definition = state.data[`${lineIndex}:${charIndex}`]; + + el.classList.add('hll'); + + setCurrentHoverElement(el); + } + + commit(types.SET_CURRENT_DEFINITION, { definition, position }); + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js new file mode 100644 index 00000000000..fe48f3ac7f5 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/index.js @@ -0,0 +1,10 @@ +import Vuex from 'vuex'; +import createState from './state'; +import actions from './actions'; +import mutations from './mutations'; + +export default new Vuex.Store({ + actions, + mutations, + state: createState(), +}); diff --git a/app/assets/javascripts/code_navigation/store/mutation_types.js b/app/assets/javascripts/code_navigation/store/mutation_types.js new file mode 100644 index 00000000000..29a2897a6fd --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutation_types.js @@ -0,0 +1,5 @@ +export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; +export const REQUEST_DATA = 'REQUEST_DATA'; +export const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS'; +export const REQUEST_DATA_ERROR = 'REQUEST_DATA_ERROR'; +export const SET_CURRENT_DEFINITION = 'SET_CURRENT_DEFINITION'; diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js new file mode 100644 index 00000000000..bb833a5adbc --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/mutations.js @@ -0,0 +1,23 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) { + state.projectPath = projectPath; + state.commitId = commitId; + state.blobPath = blobPath; + }, + [types.REQUEST_DATA](state) { + state.loading = true; + }, + [types.REQUEST_DATA_SUCCESS](state, data) { + state.loading = false; + state.data = data; + }, + [types.REQUEST_DATA_ERROR](state) { + state.loading = false; + }, + [types.SET_CURRENT_DEFINITION](state, { definition, position }) { + state.currentDefinition = definition; + state.currentDefinitionPosition = position; + }, +}; diff --git a/app/assets/javascripts/code_navigation/store/state.js b/app/assets/javascripts/code_navigation/store/state.js new file mode 100644 index 00000000000..a7b3b289db4 --- /dev/null +++ b/app/assets/javascripts/code_navigation/store/state.js @@ -0,0 +1,9 @@ +export default () => ({ + projectPath: null, + commitId: null, + blobPath: null, + loading: false, + data: null, + currentDefinition: null, + currentDefinitionPosition: null, +}); diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js new file mode 100644 index 00000000000..2dee0de6501 --- /dev/null +++ b/app/assets/javascripts/code_navigation/utils/index.js @@ -0,0 +1,20 @@ +export const cachedData = new Map(); + +export const getCurrentHoverElement = () => cachedData.get('current'); +export const setCurrentHoverElement = el => cachedData.set('current', el); + +export const addInteractionClass = d => { + let charCount = 0; + const line = document.getElementById(`LC${d.start_line + 1}`); + const el = [...line.childNodes].find(({ textContent }) => { + if (charCount === d.start_char) return true; + charCount += textContent.length; + return false; + }); + + if (el) { + el.setAttribute('data-char-index', d.start_char); + el.setAttribute('data-line-index', d.start_line); + el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation'); + } +}; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index aee67899ca2..caf9a8c0b64 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -30,4 +30,9 @@ document.addEventListener('DOMContentLoaded', () => { } GpgBadges.fetch(); + + if (gon.features?.codeNavigation) { + // eslint-disable-next-line promise/catch-or-return + import('~/code_navigation').then(m => m.default()); + } }); diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index 40ce200befb..78c355ecb76 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -46,8 +46,8 @@ export default { </a> </template> - <template v-else-if="field.type === $options.fieldTypes.miliseconds">{{ - sprintf(__('%{value} ms'), { value: field.value }) + <template v-else-if="field.type === $options.fieldTypes.seconds">{{ + sprintf(__('%{value} s'), { value: field.value }) }}</template> <template v-else-if="field.type === $options.fieldTypes.text"> diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 66ac1af062b..1845b51e6b2 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -1,7 +1,7 @@ export const fieldTypes = { codeBock: 'codeBlock', link: 'link', - miliseconds: 'miliseconds', + seconds: 'seconds', text: 'text', }; diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 25f9f70d095..d0b2d0a37f5 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -48,7 +48,7 @@ export default () => ({ execution_time: { value: null, text: s__('Reports|Execution time'), - type: fieldTypes.miliseconds, + type: fieldTypes.seconds, }, failure: { value: null, diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 5b9e3817f3a..67e5f175039 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -54,11 +54,17 @@ const populateUserInfo = user => { ); }; +const initializedPopovers = new Map(); + export default (elements = document.querySelectorAll('.js-user-link')) => { const userLinks = Array.from(elements); + const UserPopoverComponent = Vue.extend(UserPopover); return userLinks.map(el => { - const UserPopoverComponent = Vue.extend(UserPopover); + if (initializedPopovers.has(el)) { + return initializedPopovers.get(el); + } + const user = { location: null, bio: null, @@ -73,6 +79,8 @@ export default (elements = document.querySelectorAll('.js-user-link')) => { }, }); + initializedPopovers.set(el, renderedPopover); + renderedPopover.$mount(); el.addEventListener('mouseenter', ({ target }) => { diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1a017f03ebb..bb1c304b9fe 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -499,3 +499,15 @@ span.idiff { background-color: transparent; border: transparent; } + +.code-navigation { + border-bottom: 1px $gray-darkest dashed; + + &:hover { + border-bottom-color: $almost-black; + } +} + +.code-navigation-popover { + max-width: 450px; +} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index f3e6927767c..0fd6aafef0d 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -28,6 +28,13 @@ } } +@for $i from 1 through 12 { + #{'.tab-width-#{$i}'} { + -moz-tab-size: $i; + tab-size: $i; + } +} + .border-width-1px { border-width: 1px; } .border-bottom-width-1px { border-bottom-width: 1px; } .border-style-dashed { border-style: dashed; } diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 2166dd7dad7..1477d79c911 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController :time_display_relative, :time_format_in_24h, :show_whitespace_in_diffs, + :tab_width, :sourcegraph_enabled, :render_whitespace_in_code ] diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 3cd14cf845f..01e5103198b 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -29,6 +29,10 @@ class Projects::BlobController < Projects::ApplicationController before_action :validate_diff_params, only: :diff before_action :set_last_commit_sha, only: [:edit, :update] + before_action only: :show do + push_frontend_feature_flag(:code_navigation, @project) + end + def new commit unless @repository.empty? end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 6a271e93cd9..8a79217c929 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -63,6 +63,10 @@ module PreferencesHelper Gitlab::ColorSchemes.for_user(current_user).css_class end + def user_tab_width + Gitlab::TabWidth.css_class_for_user(current_user) + end + def language_choices Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] } end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 011871f373f..93c38d2f933 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -706,6 +706,10 @@ module ProjectsHelper Feature.enabled?(:vue_file_list, @project) end + def native_code_navigation_enabled?(project) + Feature.enabled?(:code_navigation, project) + end + def show_visibility_confirm_modal?(project) project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0 end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 3d098406ab1..31c813edb67 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -15,6 +15,11 @@ class DeployToken < ApplicationRecord has_many :project_deploy_tokens, inverse_of: :deploy_token has_many :projects, through: :project_deploy_tokens + has_many :group_deploy_tokens, inverse_of: :deploy_token + has_many :groups, through: :group_deploy_tokens + + validate :no_groups, unless: :group_type? + validate :no_projects, unless: :project_type? validate :ensure_at_least_one_scope validates :username, length: { maximum: 255 }, @@ -24,6 +29,7 @@ class DeployToken < ApplicationRecord message: "can contain only letters, digits, '_', '-', '+', and '.'" } + validates :deploy_token_type, presence: true enum deploy_token_type: { group_type: 1, project_type: 2 @@ -56,18 +62,31 @@ class DeployToken < ApplicationRecord end def has_access_to?(requested_project) - active? && project == requested_project + return false unless active? + return false unless holder + + holder.has_access_to?(requested_project) end # This is temporal. Currently we limit DeployToken - # to a single project, later we're going to extend - # that to be for multiple projects and namespaces. + # to a single project or group, later we're going to + # extend that to be for multiple projects and namespaces. def project strong_memoize(:project) do projects.first end end + def holder + strong_memoize(:holder) do + if project_type? + project_deploy_tokens.first + elsif group_type? + group_deploy_tokens.first + end + end + end + def expires_at expires_at = read_attribute(:expires_at) expires_at != Forever.date ? expires_at : nil @@ -92,4 +111,12 @@ class DeployToken < ApplicationRecord def default_username "gitlab+deploy-token-#{id}" if persisted? end + + def no_groups + errors.add(:deploy_token, 'cannot have groups assigned') if group_deploy_tokens.any? + end + + def no_projects + errors.add(:deploy_token, 'cannot have projects assigned') if project_deploy_tokens.any? + end end diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb new file mode 100644 index 00000000000..221a7d768ae --- /dev/null +++ b/app/models/group_deploy_token.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class GroupDeployToken < ApplicationRecord + belongs_to :group, class_name: '::Group' + belongs_to :deploy_token, inverse_of: :group_deploy_tokens + + validates :deploy_token, presence: true + validates :group, presence: true + validates :deploy_token_id, uniqueness: { scope: [:group_id] } + + def has_access_to?(requested_project) + return false unless Feature.enabled?(:allow_group_deploy_token, default: true) + + requested_project_group = requested_project&.group + return false unless requested_project_group + return true if requested_project_group.id == group_id + + requested_project_group + .ancestors + .where(id: group_id) + .exists? + end +end diff --git a/app/models/project_deploy_token.rb b/app/models/project_deploy_token.rb index a55667496fb..0bce1c745f7 100644 --- a/app/models/project_deploy_token.rb +++ b/app/models/project_deploy_token.rb @@ -7,4 +7,8 @@ class ProjectDeployToken < ApplicationRecord validates :deploy_token, presence: true validates :project, presence: true validates :deploy_token_id, uniqueness: { scope: [:project_id] } + + def has_access_to?(requested_project) + requested_project == project + end end diff --git a/app/models/user.rb b/app/models/user.rb index a5ef03215d3..3512e663f4a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,8 @@ class User < ApplicationRecord MINIMUM_INACTIVE_DAYS = 180 + enum bot_type: ::UserBotTypeEnums.bots + # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour # rubocop: disable CodeReuse/ServiceClass @@ -246,6 +248,7 @@ class User < ApplicationRecord delegate :time_display_relative, :time_display_relative=, to: :user_preference delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference + delegate :tab_width, :tab_width=, to: :user_preference delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference delegate :setup_for_company, :setup_for_company=, to: :user_preference delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference @@ -322,6 +325,8 @@ class User < ApplicationRecord scope :with_emails, -> { preload(:emails) } scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) } scope :with_public_profile, -> { where(private_profile: false) } + scope :bots, -> { where.not(bot_type: nil) } + scope :humans, -> { where(bot_type: nil) } scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do where('EXISTS (?)', @@ -598,6 +603,15 @@ class User < ApplicationRecord end end + def alert_bot + email_pattern = "alert%s@#{Settings.gitlab.host}" + + unique_internal(where(bot_type: :alert_bot), 'alert-bot', email_pattern) do |u| + u.bio = 'The GitLab alert bot' + u.name = 'GitLab Alert Bot' + end + end + # Return true if there is only single non-internal user in the deployment, # ghost user is ignored. def single_user? @@ -613,16 +627,20 @@ class User < ApplicationRecord username end + def bot? + bot_type.present? + end + def internal? - ghost? + ghost? || bot? end def self.internal - where(ghost: true) + where(ghost: true).or(bots) end def self.non_internal - without_ghosts + without_ghosts.humans end # diff --git a/app/models/user_bot_type_enums.rb b/app/models/user_bot_type_enums.rb new file mode 100644 index 00000000000..b6b08ce650b --- /dev/null +++ b/app/models/user_bot_type_enums.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module UserBotTypeEnums + def self.bots + # When adding a new key, please ensure you are not conflicting with EE-only keys in app/models/user_bot_types_enums.rb + { + alert_bot: 2 + } + end +end + +UserBotTypeEnums.prepend_if_ee('EE::UserBotTypeEnums') diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 713b0598029..48a56cded0e 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -9,7 +9,13 @@ class UserPreference < ApplicationRecord belongs_to :user validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true + validates :tab_width, numericality: { + only_integer: true, + greater_than_or_equal_to: Gitlab::TabWidth::MIN, + less_than_or_equal_to: Gitlab::TabWidth::MAX + } + default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false default_value_for :time_display_relative, value: true, allows_nil: false default_value_for :time_format_in_24h, value: false, allows_nil: false diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index c93a19bdc3d..ce3e5b0195c 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -44,6 +44,9 @@ class BasePolicy < DeclarativePolicy::Base ::Gitlab::ExternalAuthorization.perform_check? end + with_options scope: :user, score: 0 + condition(:alert_bot) { @user&.alert_bot? } + rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do prevent :read_cross_project end diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb index b963a64b429..406677d7b56 100644 --- a/app/policies/concerns/policy_actor.rb +++ b/app/policies/concerns/policy_actor.rb @@ -33,6 +33,10 @@ module PolicyActor def can_create_group false end + + def alert_bot? + false + end end PolicyActor.prepend_if_ee('EE::PolicyActor') diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index bbcb3c637a9..ee22a2d84e7 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -515,6 +515,8 @@ class ProjectPolicy < BasePolicy end def lookup_access_level! + return ::Gitlab::Access::REPORTER if alert_bot? + # NOTE: max_member_access has its own cache project.team.max_member_access(@user.id) end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7af190f5a0b..eb58115451d 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_classes } = render "layouts/head" - %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data } + %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} #{client_class_list}", data: body_data } = render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_client_detection_flags" = render 'peek/bar' diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index 91a7777514c..8d0775f6f27 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } + %body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' = header_message = render partial: "layouts/header/default", locals: { project: @project, group: @group } diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 93acd6f550b..12d42ce9892 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -69,6 +69,15 @@ = f.check_box :show_whitespace_in_diffs, class: 'form-check-input' = f.label :show_whitespace_in_diffs, class: 'form-check-label' do = s_('Preferences|Show whitespace changes in diffs') + .form-group + = f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold' + = f.number_field :tab_width, + class: 'form-control', + min: Gitlab::TabWidth::MIN, + max: Gitlab::TabWidth::MAX, + required: true + .form-text.text-muted + = s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX } .col-sm-12 %hr diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 8966dd3fd86..8397acbf1b3 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -12,5 +12,9 @@ if ('<%= current_user.layout %>' === 'fluid') { // Re-enable the "Save" button $('input[type=submit]').enable() -// Show the notice flash message -new Flash('<%= flash.discard(:notice) %>', 'notice') +// Show flash messages +<% if flash.notice %> + new Flash('<%= flash.discard(:notice) %>', 'notice') +<% elsif flash.alert %> + new Flash('<%= flash.discard(:alert) %>', 'alert') +<% end %> diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index cf273aab108..9803d65c4fb 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -9,6 +9,8 @@ = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder + - if native_code_navigation_enabled?(@project) + #js-code-navigation{ data: { commit_id: blob.commit_id, path: blob.path, project_path: @project.full_path } } %article.file-holder = render 'projects/blob/header', blob: blob = render 'projects/blob/content', blob: blob |