Commit 6de7d2c1 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 591b0e86
......@@ -10,7 +10,7 @@ notify-update-gitaly:
extends:
- .notify-slack
rules:
- if: '$CI_MERGE_REQUEST_IID && $CI_COMMIT_BRANCH == $GITALY_UPDATE_BRANCH'
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $GITALY_UPDATE_BRANCH'
when: on_failure
allow_failure: true
variables:
......
......@@ -128,6 +128,7 @@
- "{,ee/}spec/**/*.rb"
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "*_VERSION"
.db-patterns: &db-patterns
- "{,ee/}{,spec/}{db,migrations}/**/*"
......
......@@ -27,7 +27,7 @@ After your merge request has been approved according to our [approval guidelines
* At this point, it might be easy to squash the commits from the MR into one
* You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- [ ] Create each MR targeting the stable branch `X-Y-stable`, using the [Security Release merge request template].
* Every merge request will have its own set of TODOs, so make sure to complete those.
* Every merge request will have its own set of to-dos, so make sure to complete those.
- [ ] On the "Related merge requests" section, ensure that `4` merge requests are associated: The one targeting `master` and the `3` backports.
- [ ] If this issue requires less than `4` merge requests, post a message on the Security Release Tracking Issue and ping the Release Managers.
......
<script>
/* eslint-disable vue/no-v-html */
import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
GlButton,
},
mixins: [modalMixin],
props: {
newIssuePath: {
......@@ -54,17 +58,22 @@ export default {
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
__('New issue')
}}</a>
<button
<gl-button
v-if="activeTab === 'all'"
:href="newIssuePath"
category="secondary"
variant="success"
>
{{ __('New issue') }}
</gl-button>
<gl-button
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
category="primary"
variant="default"
@click="changeTab('all')"
>
{{ __('Open issues') }}
</button>
</gl-button>
</div>
</div>
</div>
......
......@@ -2,21 +2,50 @@
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
const trackingMixin = Tracking.mixin();
export default {
components: {
GlBanner,
},
inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey'],
mixins: [trackingMixin],
inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'],
data() {
return {
isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
tracking: {
label: this.trackLabel,
},
};
},
created() {
this.$nextTick(() => {
this.addTrackingAttributesToButton();
});
},
mounted() {
this.trackOnShow();
},
methods: {
handleClose() {
setCookie(this.isDismissedKey, true);
this.isDismissed = true;
this.track(this.$options.dismissEvent);
},
trackOnShow() {
if (!this.isDismissed) this.track(this.$options.displayEvent);
},
addTrackingAttributesToButton() {
if (this.$refs.banner === undefined) return;
const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`);
if (button) {
button.setAttribute('data-track-event', this.$options.buttonClickEvent);
button.setAttribute('data-track-label', this.trackLabel);
}
},
},
i18n: {
......@@ -26,6 +55,9 @@ export default {
),
button_text: s__('InviteMembersBanner|Invite your colleagues'),
},
displayEvent: 'invite_members_banner_displayed',
buttonClickEvent: 'invite_members_banner_button_clicked',
dismissEvent: 'invite_members_banner_dismissed',
};
</script>
......
......@@ -8,7 +8,7 @@ export default function initInviteMembersBanner() {
return false;
}
const { svgPath, inviteMembersPath, isDismissedKey } = el.dataset;
const { svgPath, inviteMembersPath, isDismissedKey, trackLabel } = el.dataset;
return new Vue({
el,
......@@ -16,6 +16,7 @@ export default function initInviteMembersBanner() {
svgPath,
inviteMembersPath,
isDismissedKey,
trackLabel,
},
render: createElement => createElement(InviteMembersBanner),
});
......
<script>
import { mapGetters } from 'vuex';
import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
import { deprecatedCreateFlash as flash } from '~/flash';
import { splitCamelCase } from '../../lib/utils/text_utility';
export default {
name: 'NoteActions',
......@@ -47,6 +48,26 @@ export default {
required: false,
default: null,
},
isAuthor: {
type: Boolean,
required: false,
default: false,
},
isContributor: {
type: Boolean,
required: false,
default: false,
},
noteableType: {
type: String,
required: false,
default: '',
},
projectName: {
type: String,
required: false,
default: '',
},
showReply: {
type: Boolean,
required: true,
......@@ -121,6 +142,9 @@ export default {
targetType() {
return this.getNoteableData.targetType;
},
noteableDisplayName() {
return splitCamelCase(this.noteableType).toLowerCase();
},
assignees() {
return this.getNoteableData.assignees || [];
},
......@@ -130,6 +154,22 @@ export default {
canAssign() {
return this.getNoteableData.current_user?.can_update && this.isIssue;
},
displayAuthorBadgeText() {
return sprintf(__('This user is the author of this %{noteable}.'), {
noteable: this.noteableDisplayName,
});
},
displayMemberBadgeText() {
return sprintf(__('This user is a %{access} of the %{name} project.'), {
access: this.accessLevel.toLowerCase(),
name: this.projectName,
});
},
displayContributorBadgeText() {
return sprintf(__('This user has previously committed to the %{name} project.'), {
name: this.projectName,
});
},
},
methods: {
onEdit() {
......@@ -175,7 +215,24 @@ export default {
<template>
<div class="note-actions">
<span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span>
<span
v-if="isAuthor"
class="note-role user-access-role has-tooltip d-none d-md-inline-block"
:title="displayAuthorBadgeText"
>{{ __('Author') }}</span
>
<span
v-if="accessLevel"
class="note-role user-access-role has-tooltip"
:title="displayMemberBadgeText"
>{{ accessLevel }}</span
>
<span
v-else-if="isContributor"
class="note-role user-access-role has-tooltip"
:title="displayContributorBadgeText"
>{{ __('Contributor') }}</span
>
<div v-if="canResolve" class="note-actions-item">
<button
ref="resolveButton"
......
......@@ -389,6 +389,10 @@ export default {
:note-id="note.id"
:note-url="note.noteable_note_url"
:access-level="note.human_access"
:is-contributor="note.is_contributor"
:is-author="note.is_noteable_author"
:project-name="note.project_name"
:noteable-type="note.noteable_type"
:show-reply="showReplyButton"
:can-edit="note.current_user.can_edit"
:can-award-emoji="note.current_user.can_award_emoji"
......
......@@ -143,7 +143,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
:shortcuts="['command+b', 'ctrl+b']"
shortcuts="mod+b"
icon="bold"
/>
<toolbar-button
......@@ -151,7 +151,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
:shortcuts="['command+i', 'ctrl+i']"
shortcuts="mod+i"
icon="italic"
/>
<toolbar-button
......@@ -207,7 +207,7 @@ export default {
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
:shortcuts="['command+k', 'ctrl+k']"
shortcuts="mod+k"
icon="link"
/>
</div>
......
......@@ -5,7 +5,6 @@ module RendersNotes
def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
preload_author_status(notes)
Notes::RenderService.new(current_user).execute(notes)
......@@ -19,7 +18,8 @@ def preload_max_access_for_authors(notes, project)
return unless project
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
access = project.team.max_member_access_for_user_ids(user_ids).select { |k, v| v == Gitlab::Access::NO_ACCESS }.keys
project.team.contribution_check_for_user_ids(access)
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -28,12 +28,6 @@ def preload_noteable_for_regular_notes(notes)
end
# rubocop: enable CodeReuse/ActiveRecord
def preload_first_time_contribution_for_authors(noteable, notes)
return unless noteable.is_a?(Issuable) && noteable.first_contribution?
notes.each {|n| n.specialize_for_first_contribution!(noteable)}
end
# rubocop: disable CodeReuse/ActiveRecord
def preload_author_status(notes)
ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status })
......
......@@ -205,6 +205,12 @@ def issuable_meta(issuable, project, text)
author_output
end
if access = project.team.human_max_access(issuable.author_id)
output << content_tag(:span, access, class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3 ", title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: project.name })
elsif project.team.contributor?(issuable.author_id)
output << content_tag(:span, _("Contributor"), class: "user-access-role has-tooltip d-none d-xl-inline-block gl-ml-3", title: _("This user has previously committed to the %{name} project.") % { name: project.name })
end
output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3")
......
......@@ -85,6 +85,10 @@ def note_max_access_for_user(note)
note.project.team.max_member_access(note.author_id)
end
def note_human_max_access(note)
note.project.team.human_max_access(note.author_id)
end
def discussion_path(discussion)
if discussion.for_merge_request?
return unless discussion.diff_discussion?
......
......@@ -410,10 +410,17 @@ def project_users_with_descendants
.where(namespaces: { id: self_and_descendants.select(:id) })
end
def max_member_access_for_user(user)
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
# which implies it has "OWNER" access everywhere, but should not
# officially appear as a member of a group unless specifically added to it
#
# @param user [User]
# @param only_concrete_membership [Bool] whether require admin concrete membership status
def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.admin?
return GroupMember::OWNER if user.admin? && !only_concrete_membership
max_member_access = members_with_parents.where(user_id: user)
.reorder(access_level: :desc)
......
......@@ -1603,7 +1603,7 @@ def update_project_counter_caches
def first_contribution?
return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
project.merge_requests.merged.where(author_id: author_id).empty?
!project.merge_requests.merged.exists?(author_id: author_id)
end
# TODO: remove once production database rename completes
......
......@@ -20,20 +20,6 @@ class Note < ApplicationRecord
include ThrottledTouch
include FromUnion
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
class << self
def values
constants.map {|const| self.const_get(const, false)}
end
def value?(val)
values.include?(val)
end
end
end
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
redact_field :note
......@@ -60,9 +46,6 @@ def value?(val)
# Attribute used to store the attributes that have been changed by quick actions.
attr_accessor :commands_changes
# A special role that may be displayed on issuable's discussions
attr_reader :special_role
default_value_for :system, false
attr_mentionable :note, pipeline: :note
......@@ -220,10 +203,6 @@ def count_for_collection(ids, type)
.where(noteable_type: type, noteable_id: ids)
end
def has_special_role?(role, note)
note.special_role == role
end
def search(query)
fuzzy_search(query, [:note])
end
......@@ -342,20 +321,20 @@ def noteable_assignee_or_author?(user)
noteable.author_id == user.id
end
def special_role=(role)
raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.value?(role)
def contributor?
return false unless ::Feature.enabled?(:show_contributor_on_note, project)
@special_role = role
project&.team&.contributor?(self.author_id)
end
def has_special_role?(role)
self.class.has_special_role?(role, self)
end
def noteable_author?(noteable)
return false unless ::Feature.enabled?(:show_author_on_note, project)
def specialize_for_first_contribution!(noteable)
return unless noteable.author_id == self.author_id
noteable.author == self.author
end
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
def project_name
project&.name
end
def confidential?(include_noteable: false)
......
......@@ -178,6 +178,40 @@ def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
end
def contribution_check_for_user_ids(user_ids)
user_ids = user_ids.uniq
key = "contribution_check_for_users:#{project.id}"
Gitlab::SafeRequestStore[key] ||= {}
contributors = Gitlab::SafeRequestStore[key] || {}
user_ids -= contributors.keys
return contributors if user_ids.empty?
resource_contributors = project.merge_requests
.merged
.where(author_id: user_ids, target_branch: project.default_branch.to_s)
.pluck(:author_id)
.product([true]).to_h
contributors.merge!(resource_contributors)
missing_resource_ids = user_ids - resource_contributors.keys
missing_resource_ids.each do |resource_id|
contributors[resource_id] = false
end
contributors
end
def contributor?(user_id)
return false if max_member_access(user_id) >= Gitlab::Access::GUEST
contribution_check_for_user_ids([user_id])[user_id]
end
private
def fetch_members(level = nil)
......
......@@ -46,6 +46,10 @@ class NoteEntity < API::Entities::Note
SystemNoteHelper.system_note_icon_name(note)
end
expose :is_noteable_author do |note|
note.noteable_author?(request.noteable)
end
expose :discussion_id do |note|
note.discussion_id(request.noteable)
end
......
......@@ -5,6 +5,14 @@ class ProjectNoteEntity < NoteEntity
note.project.team.human_max_access(note.author_id)
end
expose :is_contributor, if: -> (note, _) { note.project.present? } do |note|
note.contributor?
end
expose :project_name, if: -> (note, _) { note.project.present? } do |note|
note.project.name
end
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
toggle_award_emoji_project_note_path(note.project, note.id)
end
......
......@@ -114,8 +114,13 @@ def after_create_actions
# completes), and any other affected users in the background
def setup_authorizations
if @project.group
current_user.project_authorizations.create!(project: @project,
access_level: @project.group.max_member_access_for_user(current_user))
group_access_level = @project.group.max_member_access_for_user(current_user,
only_concrete_membership: true)
if group_access_level > GroupMember::NO_ACCESS
current_user.project_authorizations.create!(project: @project,
access_level: group_access_level)
end
if Feature.enabled?(:specialized_project_authorization_workers)
AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
......
......@@ -7,6 +7,7 @@
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
.js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
track_label: 'invite_members_banner',
invite_members_path: group_group_members_path(@group) } }
= content_for :meta_tags do
......
- access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-top')
- if access.nonzero?
%span.note-role.user-access-role= Gitlab::Access.human_access(access)
- access = note_human_max_access(note)
- if note.noteable_author?(@noteable)
%span{ class: 'note-role user-access-role has-tooltip d-none d-md-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name } }= _("Author")
- if access
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user is a %{access} of the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
- elsif note.contributor?
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
......