Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 15:10:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-06 15:10:29 +0300
commit5564275a0b378298dc6281599cbfe71a937109ff (patch)
treea468e1e60046356410219c35c23a8a428c5e2c5e /app
parentd87918510a866a5fcbbc2f899ad65c6938ebf5f5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/boards/components/issue_count.vue2
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue43
-rw-r--r--app/assets/javascripts/code_navigation/components/popover.vue76
-rw-r--r--app/assets/javascripts/code_navigation/index.js20
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js62
-rw-r--r--app/assets/javascripts/code_navigation/store/index.js10
-rw-r--r--app/assets/javascripts/code_navigation/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/code_navigation/store/mutations.js23
-rw-r--r--app/assets/javascripts/code_navigation/store/state.js9
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js20
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js5
-rw-r--r--app/assets/javascripts/reports/components/modal.vue4
-rw-r--r--app/assets/javascripts/reports/constants.js2
-rw-r--r--app/assets/javascripts/reports/store/state.js2
-rw-r--r--app/assets/javascripts/user_popovers.js10
-rw-r--r--app/assets/stylesheets/framework/files.scss12
-rw-r--r--app/assets/stylesheets/utilities.scss7
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb4
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/models/deploy_token.rb33
-rw-r--r--app/models/group_deploy_token.rb23
-rw-r--r--app/models/project_deploy_token.rb4
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/user_bot_type_enums.rb12
-rw-r--r--app/models/user_preference.rb6
-rw-r--r--app/policies/base_policy.rb3
-rw-r--r--app/policies/concerns/policy_actor.rb4
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml9
-rw-r--r--app/views/profiles/preferences/update.js.erb8
-rw-r--r--app/views/projects/blob/_blob.html.haml2
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