Merge branch 'separate-banzai-references' into 'master'
Separate reference gathering from rendering This is a required step to allow batch processing when gathering references. This in turn would allow grabbing (for example) all mentioned users of an issue/merge request using a single query. cc @rspeicher @DouweM See merge request !3969
This commit is contained in:
commit
2d084dd848
93 changed files with 2133 additions and 616 deletions
|
@ -91,8 +91,8 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
def markdown_preview
|
||||
text = params[:text]
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
|
||||
ext.analyze(text)
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
ext.analyze(text, author: current_user)
|
||||
|
||||
render json: {
|
||||
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki),
|
||||
|
|
|
@ -197,8 +197,8 @@ class ProjectsController < Projects::ApplicationController
|
|||
def markdown_preview
|
||||
text = params[:text]
|
||||
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
|
||||
ext.analyze(text)
|
||||
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
|
||||
ext.analyze(text, author: current_user)
|
||||
|
||||
render json: {
|
||||
body: view_context.markdown(text),
|
||||
|
|
|
@ -23,6 +23,28 @@ class Ability
|
|||
end.concat(global_abilities(user))
|
||||
end
|
||||
|
||||
# Given a list of users and a project this method returns the users that can
|
||||
# read the given project.
|
||||
def users_that_can_read_project(users, project)
|
||||
if project.public?
|
||||
users
|
||||
else
|
||||
users.select do |user|
|
||||
if user.admin?
|
||||
true
|
||||
elsif project.internal? && !user.external?
|
||||
true
|
||||
elsif project.owner == user
|
||||
true
|
||||
elsif project.team.members.include?(user)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# List of possible abilities for anonymous user
|
||||
def anonymous_abilities(user, subject)
|
||||
if subject.is_a?(PersonalSnippet)
|
||||
|
|
|
@ -8,7 +8,10 @@ class Commit
|
|||
include StaticModel
|
||||
|
||||
attr_mentionable :safe_message, pipeline: :single_line
|
||||
participant :author, :committer, :notes
|
||||
|
||||
participant :author
|
||||
participant :committer
|
||||
participant :notes_with_associations
|
||||
|
||||
attr_accessor :project
|
||||
|
||||
|
@ -194,6 +197,10 @@ class Commit
|
|||
project.notes.for_commit_id(self.id)
|
||||
end
|
||||
|
||||
def notes_with_associations
|
||||
notes.includes(:author, :project)
|
||||
end
|
||||
|
||||
def method_missing(m, *args, &block)
|
||||
@raw.send(m, *args, &block)
|
||||
end
|
||||
|
@ -219,7 +226,7 @@ class Commit
|
|||
def revert_branch_name
|
||||
"revert-#{short_id}"
|
||||
end
|
||||
|
||||
|
||||
def cherry_pick_branch_name
|
||||
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
|
||||
end
|
||||
|
@ -251,11 +258,13 @@ class Commit
|
|||
end
|
||||
|
||||
def has_been_reverted?(current_user = nil, noteable = self)
|
||||
Gitlab::ReferenceExtractor.lazily do
|
||||
noteable.notes.system.flat_map do |note|
|
||||
note.all_references(current_user).commits
|
||||
end
|
||||
end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
|
||||
ext = all_references(current_user)
|
||||
|
||||
noteable.notes_with_associations.system.each do |note|
|
||||
note.all_references(current_user, extractor: ext)
|
||||
end
|
||||
|
||||
ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) }
|
||||
end
|
||||
|
||||
def change_type_title
|
||||
|
|
|
@ -62,7 +62,7 @@ class CommitRange
|
|||
def initialize(range_string, project)
|
||||
@project = project
|
||||
|
||||
range_string.strip!
|
||||
range_string = range_string.strip
|
||||
|
||||
unless range_string =~ /\A#{PATTERN}\z/
|
||||
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
|
||||
|
|
|
@ -59,8 +59,12 @@ module Issuable
|
|||
prefix: true
|
||||
|
||||
attr_mentionable :title, pipeline: :single_line
|
||||
attr_mentionable :description, cache: true
|
||||
participant :author, :assignee, :notes_with_associations
|
||||
attr_mentionable :description
|
||||
|
||||
participant :author
|
||||
participant :assignee
|
||||
participant :notes_with_associations
|
||||
|
||||
strip_attributes :title
|
||||
|
||||
acts_as_paranoid
|
||||
|
|
|
@ -23,7 +23,7 @@ module Mentionable
|
|||
|
||||
included do
|
||||
if self < Participable
|
||||
participant ->(current_user) { mentioned_users(current_user) }
|
||||
participant -> (user, ext) { all_references(user, extractor: ext) }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -43,23 +43,22 @@ module Mentionable
|
|||
self
|
||||
end
|
||||
|
||||
def all_references(current_user = nil, text = nil)
|
||||
ext = Gitlab::ReferenceExtractor.new(self.project, current_user || self.author, self.author)
|
||||
def all_references(current_user = nil, text = nil, extractor: nil)
|
||||
extractor ||= Gitlab::ReferenceExtractor.
|
||||
new(project, current_user || author)
|
||||
|
||||
if text
|
||||
ext.analyze(text)
|
||||
extractor.analyze(text, author: author)
|
||||
else
|
||||
self.class.mentionable_attrs.each do |attr, options|
|
||||
text = send(attr)
|
||||
text = __send__(attr)
|
||||
options = options.merge(cache_key: [self, attr], author: author)
|
||||
|
||||
context = options.dup
|
||||
context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
|
||||
|
||||
ext.analyze(text, context)
|
||||
extractor.analyze(text, options)
|
||||
end
|
||||
end
|
||||
|
||||
ext
|
||||
extractor
|
||||
end
|
||||
|
||||
def mentioned_users(current_user = nil)
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
# Contains functionality related to objects that can have participants, such as
|
||||
# an author, an assignee and people mentioned in its description or comments.
|
||||
#
|
||||
# Used by Issue, Note, MergeRequest, Snippet and Commit.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# class Issue < ActiveRecord::Base
|
||||
|
@ -12,22 +10,36 @@
|
|||
#
|
||||
# # ...
|
||||
#
|
||||
# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) }
|
||||
# participant :author
|
||||
# participant :assignee
|
||||
# participant :notes
|
||||
#
|
||||
# participant -> (current_user, ext) do
|
||||
# ext.analyze('...')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# issue = Issue.last
|
||||
# users = issue.participants
|
||||
# # `users` will contain the issue's author, its assignee,
|
||||
# # all users returned by its #mentioned_users method,
|
||||
# # as well as all participants to all of the issue's notes,
|
||||
# # since Note implements Participable as well.
|
||||
#
|
||||
module Participable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
def participant(*attrs)
|
||||
participant_attrs.concat(attrs)
|
||||
# Adds a list of participant attributes. Attributes can either be symbols or
|
||||
# Procs.
|
||||
#
|
||||
# When using a Proc instead of a Symbol the Proc will be given two
|
||||
# arguments:
|
||||
#
|
||||
# 1. The current user (as an instance of User)
|
||||
# 2. An instance of `Gitlab::ReferenceExtractor`
|
||||
#
|
||||
# It is expected that a Proc populates the given reference extractor
|
||||
# instance with data. The return value of the Proc is ignored.
|
||||
#
|
||||
# attr - The name of the attribute or a Proc
|
||||
def participant(attr)
|
||||
participant_attrs << attr
|
||||
end
|
||||
|
||||
def participant_attrs
|
||||
|
@ -35,42 +47,42 @@ module Participable
|
|||
end
|
||||
end
|
||||
|
||||
# Be aware that this method makes a lot of sql queries.
|
||||
# Save result into variable if you are going to reuse it inside same request
|
||||
def participants(current_user = self.author)
|
||||
participants =
|
||||
Gitlab::ReferenceExtractor.lazily do
|
||||
self.class.participant_attrs.flat_map do |attr|
|
||||
value =
|
||||
if attr.respond_to?(:call)
|
||||
instance_exec(current_user, &attr)
|
||||
else
|
||||
send(attr)
|
||||
end
|
||||
# Returns the users participating in a discussion.
|
||||
#
|
||||
# This method processes attributes of objects in breadth-first order.
|
||||
#
|
||||
# Returns an Array of User instances.
|
||||
def participants(current_user = nil)
|
||||
current_user ||= author
|
||||
ext = Gitlab::ReferenceExtractor.new(project, current_user)
|
||||
participants = Set.new
|
||||
process = [self]
|
||||
|
||||
participants_for(value, current_user)
|
||||
end.compact.uniq
|
||||
end
|
||||
until process.empty?
|
||||
source = process.pop
|
||||
|
||||
unless Gitlab::ReferenceExtractor.lazy?
|
||||
participants.select! do |user|
|
||||
user.can?(:read_project, project)
|
||||
case source
|
||||
when User
|
||||
participants << source
|
||||
when Participable
|
||||
source.class.participant_attrs.each do |attr|
|
||||
if attr.respond_to?(:call)
|
||||
source.instance_exec(current_user, ext, &attr)
|
||||
else
|
||||
process << source.__send__(attr)
|
||||
end
|
||||
end
|
||||
when Enumerable, ActiveRecord::Relation
|
||||
# This uses reverse_each so we can use "pop" to get the next value to
|
||||
# process (in order). Using unshift instead of pop would require
|
||||
# moving all Array values one index to the left (which can be
|
||||
# expensive).
|
||||
source.reverse_each { |obj| process << obj }
|
||||
end
|
||||
end
|
||||
|
||||
participants
|
||||
end
|
||||
participants.merge(ext.users)
|
||||
|
||||
private
|
||||
|
||||
def participants_for(value, current_user = nil)
|
||||
case value
|
||||
when User, Banzai::LazyReference
|
||||
[value]
|
||||
when Enumerable, ActiveRecord::Relation
|
||||
value.flat_map { |v| participants_for(v, current_user) }
|
||||
when Participable
|
||||
value.participants(current_user)
|
||||
end
|
||||
Ability.users_that_can_read_project(participants.to_a, project)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,14 +95,13 @@ class Issue < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def referenced_merge_requests(current_user = nil)
|
||||
@referenced_merge_requests ||= {}
|
||||
@referenced_merge_requests[current_user] ||= begin
|
||||
Gitlab::ReferenceExtractor.lazily do
|
||||
[self, *notes].flat_map do |note|
|
||||
note.all_references(current_user).merge_requests
|
||||
end
|
||||
end.sort_by(&:iid).uniq
|
||||
ext = all_references(current_user)
|
||||
|
||||
notes_with_associations.each do |object|
|
||||
object.all_references(current_user, extractor: ext)
|
||||
end
|
||||
|
||||
ext.merge_requests.sort_by(&:iid)
|
||||
end
|
||||
|
||||
# All branches containing the current issue's ID, except for
|
||||
|
@ -139,9 +138,13 @@ class Issue < ActiveRecord::Base
|
|||
def closed_by_merge_requests(current_user = nil)
|
||||
return [] unless open?
|
||||
|
||||
notes.system.flat_map do |note|
|
||||
note.all_references(current_user).merge_requests
|
||||
end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
|
||||
ext = all_references(current_user)
|
||||
|
||||
notes.system.each do |note|
|
||||
note.all_references(current_user, extractor: ext)
|
||||
end
|
||||
|
||||
ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
|
||||
end
|
||||
|
||||
def moved?
|
||||
|
|
|
@ -6,7 +6,7 @@ class Note < ActiveRecord::Base
|
|||
|
||||
default_value_for :system, false
|
||||
|
||||
attr_mentionable :note, cache: true, pipeline: :note
|
||||
attr_mentionable :note, pipeline: :note
|
||||
participant :author
|
||||
|
||||
belongs_to :project
|
||||
|
|
|
@ -7,5 +7,6 @@ class ProjectSnippet < Snippet
|
|||
# Scopes
|
||||
scope :fresh, -> { order("created_at DESC") }
|
||||
|
||||
participant :author, :notes
|
||||
participant :author
|
||||
participant :notes_with_associations
|
||||
end
|
||||
|
|
|
@ -30,7 +30,8 @@ class Snippet < ActiveRecord::Base
|
|||
scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) }
|
||||
scope :fresh, -> { order("created_at DESC") }
|
||||
|
||||
participant :author, :notes
|
||||
participant :author
|
||||
participant :notes_with_associations
|
||||
|
||||
def self.reference_prefix
|
||||
'$'
|
||||
|
@ -100,6 +101,10 @@ class Snippet < ActiveRecord::Base
|
|||
content.lines.count > 1000
|
||||
end
|
||||
|
||||
def notes_with_associations
|
||||
notes.includes(:author, :project)
|
||||
end
|
||||
|
||||
class << self
|
||||
# Searches for snippets with a matching title or file name.
|
||||
#
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
.light.small
|
||||
= time_ago_with_tooltip(abuse_report.created_at)
|
||||
%td
|
||||
= markdown(abuse_report.message.squish!, pipeline: :single_line)
|
||||
= markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
|
||||
%td
|
||||
- if user
|
||||
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
.commit-row-title
|
||||
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id])
|
||||
·
|
||||
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
|
||||
= markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
%div{xmlns: "http://www.w3.org/1999/xhtml"}
|
||||
= markdown(issue.description, pipeline: :atom, project: issue.project)
|
||||
= markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
%div{xmlns: "http://www.w3.org/1999/xhtml"}
|
||||
= markdown(merge_request.description, pipeline: :atom, project: merge_request.project)
|
||||
= markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author)
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
%div{xmlns: "http://www.w3.org/1999/xhtml"}
|
||||
= markdown(note.note, pipeline: :atom, project: note.project)
|
||||
= markdown(note.note, pipeline: :atom, project: note.project, author: note.author)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
%i
|
||||
at
|
||||
= commit[:timestamp].to_time.to_s(:short)
|
||||
%blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project)
|
||||
%blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author)
|
||||
- if event.commits_count > 15
|
||||
%p
|
||||
%i
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
%ul.well-list.event_commits
|
||||
- few_commits = event.commits[0...2]
|
||||
- few_commits.each do |commit|
|
||||
= render "events/commit", commit: commit, project: project
|
||||
= render "events/commit", commit: commit, project: project, event: event
|
||||
|
||||
- create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project)
|
||||
- if event.commits_count > 1
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
%div
|
||||
#{link_to @note.author_name, user_url(@note.author)} wrote:
|
||||
%div
|
||||
= markdown(@note.note, pipeline: :email)
|
||||
= markdown(@note.note, pipeline: :email, author: @note.author)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
%div
|
||||
#{link_to @issue.author_name, user_url(@issue.author)} wrote:
|
||||
-if @issue.description
|
||||
= markdown(@issue.description, pipeline: :email)
|
||||
= markdown(@issue.description, pipeline: :email, author: @issue.author)
|
||||
|
||||
- if @issue.assignee_id.present?
|
||||
%p
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name}
|
||||
|
||||
-if @merge_request.description
|
||||
= markdown(@merge_request.description, pipeline: :email)
|
||||
= markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
|
||||
|
|
|
@ -63,10 +63,10 @@
|
|||
|
||||
.commit-box.content-block
|
||||
%h3.commit-title
|
||||
= markdown escape_once(@commit.title), pipeline: :single_line
|
||||
= markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
|
||||
- if @commit.description.present?
|
||||
%pre.commit-description
|
||||
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
|
||||
= preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
|
||||
|
||||
:javascript
|
||||
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
- if commit.description?
|
||||
.commit-row-description.js-toggle-content
|
||||
%pre
|
||||
= preserve(markdown(escape_once(commit.description), pipeline: :single_line))
|
||||
= preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
|
||||
|
||||
.commit-row-info
|
||||
by
|
||||
|
|
|
@ -52,12 +52,12 @@
|
|||
.issue-details.issuable-details
|
||||
.detail-page-description.content-block
|
||||
%h2.title
|
||||
= markdown escape_once(@issue.title), pipeline: :single_line
|
||||
= markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
|
||||
- if @issue.description.present?
|
||||
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@issue.description, cache_key: [@issue, "description"])
|
||||
= markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
|
||||
%textarea.hidden.js-task-list-field
|
||||
= @issue.description
|
||||
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
.detail-page-description.content-block
|
||||
%h2.title
|
||||
= markdown escape_once(@merge_request.title), pipeline: :single_line
|
||||
= markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
|
||||
|
||||
%div
|
||||
- if @merge_request.description.present?
|
||||
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
|
||||
.wiki
|
||||
= preserve do
|
||||
= markdown(@merge_request.description, cache_key: [@merge_request, "description"])
|
||||
= markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
|
||||
%textarea.hidden.js-task-list-field
|
||||
= @merge_request.description
|
||||
|
||||
|
|
|
@ -26,4 +26,4 @@
|
|||
%i.fa.fa-check
|
||||
Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)}
|
||||
= succeed '.' do
|
||||
!= markdown issues_sentence(@closes_issues), pipeline: :gfm
|
||||
!= markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
.note-body{class: note_editable ? 'js-task-list-container' : ''}
|
||||
.note-text
|
||||
= preserve do
|
||||
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
|
||||
= markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author)
|
||||
- if note_editable
|
||||
= render 'projects/notes/edit_form', note: note
|
||||
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
|
||||
%code= commit.short_id
|
||||
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
|
||||
= markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line
|
||||
= markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
|
||||
%td
|
||||
%span.pull-right.cgray
|
||||
= time_ago_with_tooltip(commit.committed_date)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
- if issue.description.present?
|
||||
.description.term
|
||||
= preserve do
|
||||
= search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project }))
|
||||
= search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
|
||||
%span.light
|
||||
#{issue.project.name_with_namespace}
|
||||
- if issue.closed?
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
- if merge_request.description.present?
|
||||
.description.term
|
||||
= preserve do
|
||||
= search_md_sanitize(markdown(merge_request.description, { project: merge_request.project }))
|
||||
= search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
|
||||
%span.light
|
||||
#{merge_request.project.name_with_namespace}
|
||||
.pull-right
|
||||
|
|
|
@ -19,4 +19,4 @@
|
|||
.note-search-result
|
||||
.term
|
||||
= preserve do
|
||||
= search_md_sanitize(markdown(note.note, {no_header_anchors: true}))
|
||||
= search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
|
||||
|
|
|
@ -21,4 +21,4 @@
|
|||
|
||||
.content-block.second-block
|
||||
%h2.snippet-title.prepend-top-0.append-bottom-0
|
||||
= markdown escape_once(@snippet.title), pipeline: :single_line
|
||||
= markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
|
||||
|
|
|
@ -18,10 +18,6 @@ module Banzai
|
|||
@object_sym ||= object_name.to_sym
|
||||
end
|
||||
|
||||
def self.data_reference
|
||||
@data_reference ||= "data-#{object_name.dasherize}"
|
||||
end
|
||||
|
||||
def self.object_class_title
|
||||
@object_title ||= object_class.name.titleize
|
||||
end
|
||||
|
@ -45,10 +41,6 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
|
||||
end
|
||||
|
||||
def object_class
|
||||
self.class.object_class
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ module Banzai
|
|||
#
|
||||
# This filter supports cross-project references.
|
||||
class CommitRangeReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :commit_range
|
||||
|
||||
def self.object_class
|
||||
CommitRange
|
||||
end
|
||||
|
@ -14,34 +16,18 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
project = Project.find(node.attr("data-project")) rescue nil
|
||||
return unless project
|
||||
|
||||
id = node.attr("data-commit-range")
|
||||
range = find_object(project, id)
|
||||
|
||||
return unless range
|
||||
|
||||
{ commit_range: range }
|
||||
end
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
|
||||
@commit_map = {}
|
||||
end
|
||||
|
||||
def self.find_object(project, id)
|
||||
def find_object(project, id)
|
||||
range = CommitRange.new(id, project)
|
||||
|
||||
range.valid_commits? ? range : nil
|
||||
end
|
||||
|
||||
def find_object(*args)
|
||||
self.class.find_object(*args)
|
||||
end
|
||||
|
||||
def url_for_object(range, project)
|
||||
h = Gitlab::Routing.url_helpers
|
||||
h.namespace_project_compare_url(project.namespace, project,
|
||||
|
|
|
@ -4,6 +4,8 @@ module Banzai
|
|||
#
|
||||
# This filter supports cross-project references.
|
||||
class CommitReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :commit
|
||||
|
||||
def self.object_class
|
||||
Commit
|
||||
end
|
||||
|
@ -14,28 +16,12 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
project = Project.find(node.attr("data-project")) rescue nil
|
||||
return unless project
|
||||
|
||||
id = node.attr("data-commit")
|
||||
commit = find_object(project, id)
|
||||
|
||||
return unless commit
|
||||
|
||||
{ commit: commit }
|
||||
end
|
||||
|
||||
def self.find_object(project, id)
|
||||
def find_object(project, id)
|
||||
if project && project.valid_repo?
|
||||
project.commit(id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_object(*args)
|
||||
self.class.find_object(*args)
|
||||
end
|
||||
|
||||
def url_for_object(commit, project)
|
||||
h = Gitlab::Routing.url_helpers
|
||||
h.namespace_project_commit_url(project.namespace, project, commit,
|
||||
|
|
|
@ -4,6 +4,8 @@ module Banzai
|
|||
# References are ignored if the project doesn't use an external issue
|
||||
# tracker.
|
||||
class ExternalIssueReferenceFilter < ReferenceFilter
|
||||
self.reference_type = :external_issue
|
||||
|
||||
# Public: Find `JIRA-123` issue references in text
|
||||
#
|
||||
# ExternalIssueReferenceFilter.references_in(text) do |match, issue|
|
||||
|
@ -21,18 +23,6 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
project = Project.find(node.attr("data-project")) rescue nil
|
||||
return unless project
|
||||
|
||||
id = node.attr("data-external-issue")
|
||||
external_issue = ExternalIssue.new(id, project)
|
||||
|
||||
return unless external_issue
|
||||
|
||||
{ external_issue: external_issue }
|
||||
end
|
||||
|
||||
def call
|
||||
# Early return if the project isn't using an external tracker
|
||||
return doc if project.nil? || default_issues_tracker?
|
||||
|
|
|
@ -5,18 +5,12 @@ module Banzai
|
|||
#
|
||||
# This filter supports cross-project references.
|
||||
class IssueReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :issue
|
||||
|
||||
def self.object_class
|
||||
Issue
|
||||
end
|
||||
|
||||
def self.user_can_see_reference?(user, node, context)
|
||||
# It is not possible to check access rights for external issue trackers
|
||||
return true if context[:project].try(:external_issue_tracker)
|
||||
|
||||
issue = Issue.find(node.attr('data-issue')) rescue nil
|
||||
Ability.abilities.allowed?(user, :read_issue, issue)
|
||||
end
|
||||
|
||||
def find_object(project, id)
|
||||
project.get_issue(id)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ module Banzai
|
|||
module Filter
|
||||
# HTML filter that replaces label references with links.
|
||||
class LabelReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :label
|
||||
|
||||
def self.object_class
|
||||
Label
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Banzai
|
|||
#
|
||||
# This filter supports cross-project references.
|
||||
class MergeRequestReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :merge_request
|
||||
|
||||
def self.object_class
|
||||
MergeRequest
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ module Banzai
|
|||
module Filter
|
||||
# HTML filter that replaces milestone references with links.
|
||||
class MilestoneReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :milestone
|
||||
|
||||
def self.object_class
|
||||
Milestone
|
||||
end
|
||||
|
|
|
@ -7,8 +7,11 @@ module Banzai
|
|||
#
|
||||
class RedactorFilter < HTML::Pipeline::Filter
|
||||
def call
|
||||
Querying.css(doc, 'a.gfm').each do |node|
|
||||
unless user_can_see_reference?(node)
|
||||
nodes = Querying.css(doc, 'a.gfm[data-reference-type]')
|
||||
visible = nodes_visible_to_user(nodes)
|
||||
|
||||
nodes.each do |node|
|
||||
unless visible.include?(node)
|
||||
# The reference should be replaced by the original text,
|
||||
# which is not always the same as the rendered text.
|
||||
text = node.attr('data-original') || node.text
|
||||
|
@ -21,20 +24,30 @@ module Banzai
|
|||
|
||||
private
|
||||
|
||||
def user_can_see_reference?(node)
|
||||
if node.has_attribute?('data-reference-filter')
|
||||
reference_type = node.attr('data-reference-filter')
|
||||
reference_filter = Banzai::Filter.const_get(reference_type)
|
||||
def nodes_visible_to_user(nodes)
|
||||
per_type = Hash.new { |h, k| h[k] = [] }
|
||||
visible = Set.new
|
||||
|
||||
reference_filter.user_can_see_reference?(current_user, node, context)
|
||||
else
|
||||
true
|
||||
nodes.each do |node|
|
||||
per_type[node.attr('data-reference-type')] << node
|
||||
end
|
||||
|
||||
per_type.each do |type, nodes|
|
||||
parser = Banzai::ReferenceParser[type].new(project, current_user)
|
||||
|
||||
visible.merge(parser.nodes_visible_to_user(current_user, nodes))
|
||||
end
|
||||
|
||||
visible
|
||||
end
|
||||
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def project
|
||||
context[:project]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,24 +8,8 @@ module Banzai
|
|||
# :project (required) - Current project, ignored if reference is cross-project.
|
||||
# :only_path - Generate path-only links.
|
||||
class ReferenceFilter < HTML::Pipeline::Filter
|
||||
def self.user_can_see_reference?(user, node, context)
|
||||
if node.has_attribute?('data-project')
|
||||
project_id = node.attr('data-project').to_i
|
||||
return true if project_id == context[:project].try(:id)
|
||||
|
||||
project = Project.find(project_id) rescue nil
|
||||
Ability.abilities.allowed?(user, :read_project, project)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def self.user_can_reference?(user, node, context)
|
||||
true
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
raise NotImplementedError, "#{self} does not implement #{__method__}"
|
||||
class << self
|
||||
attr_accessor :reference_type
|
||||
end
|
||||
|
||||
# Returns a data attribute String to attach to a reference link
|
||||
|
@ -43,7 +27,9 @@ module Banzai
|
|||
#
|
||||
# Returns a String
|
||||
def data_attribute(attributes = {})
|
||||
attributes[:reference_filter] = self.class.name.demodulize
|
||||
attributes = attributes.reject { |_, v| v.nil? }
|
||||
|
||||
attributes[:reference_type] = self.class.reference_type
|
||||
attributes.delete(:original) if context[:no_original_data]
|
||||
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
|
||||
end
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that gathers all referenced records that the current user has
|
||||
# permission to view.
|
||||
#
|
||||
# Expected to be run in its own post-processing pipeline.
|
||||
#
|
||||
class ReferenceGathererFilter < HTML::Pipeline::Filter
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
|
||||
end
|
||||
|
||||
def call
|
||||
Querying.css(doc, 'a.gfm').each do |node|
|
||||
gather_references(node)
|
||||
end
|
||||
|
||||
load_lazy_references unless ReferenceExtractor.lazy?
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gather_references(node)
|
||||
return unless node.has_attribute?('data-reference-filter')
|
||||
|
||||
reference_type = node.attr('data-reference-filter')
|
||||
reference_filter = Banzai::Filter.const_get(reference_type)
|
||||
|
||||
return if context[:reference_filter] && reference_filter != context[:reference_filter]
|
||||
|
||||
return if author && !reference_filter.user_can_reference?(author, node, context)
|
||||
|
||||
return unless reference_filter.user_can_see_reference?(current_user, node, context)
|
||||
|
||||
references = reference_filter.referenced_by(node)
|
||||
return unless references
|
||||
|
||||
references.each do |type, values|
|
||||
Array.wrap(values).each do |value|
|
||||
result[:references][type] << value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_lazy_references
|
||||
refs = result[:references]
|
||||
refs.each do |type, values|
|
||||
refs[type] = ReferenceExtractor.lazily(values)
|
||||
end
|
||||
end
|
||||
|
||||
def current_user
|
||||
context[:current_user]
|
||||
end
|
||||
|
||||
def author
|
||||
context[:author]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,8 @@ module Banzai
|
|||
#
|
||||
# This filter supports cross-project references.
|
||||
class SnippetReferenceFilter < AbstractReferenceFilter
|
||||
self.reference_type = :snippet
|
||||
|
||||
def self.object_class
|
||||
Snippet
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ module Banzai
|
|||
#
|
||||
# A special `@all` reference is also supported.
|
||||
class UserReferenceFilter < ReferenceFilter
|
||||
self.reference_type = :user
|
||||
|
||||
# Public: Find `@user` user references in text
|
||||
#
|
||||
# UserReferenceFilter.references_in(text) do |match, username|
|
||||
|
@ -21,43 +23,6 @@ module Banzai
|
|||
end
|
||||
end
|
||||
|
||||
def self.referenced_by(node)
|
||||
if node.has_attribute?('data-group')
|
||||
group = Group.find(node.attr('data-group')) rescue nil
|
||||
return unless group
|
||||
|
||||
{ user: group.users }
|
||||
elsif node.has_attribute?('data-user')
|
||||
{ user: LazyReference.new(User, node.attr('data-user')) }
|
||||
elsif node.has_attribute?('data-project')
|
||||
project = Project.find(node.attr('data-project')) rescue nil
|
||||
return unless project
|
||||
|
||||
{ user: project.team.members.flatten }
|
||||
end
|
||||
end
|
||||
|
||||
def self.user_can_see_reference?(user, node, context)
|
||||
if node.has_attribute?('data-group')
|
||||
group = Group.find(node.attr('data-group')) rescue nil
|
||||
Ability.abilities.allowed?(user, :read_group, group)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def self.user_can_reference?(user, node, context)
|
||||
# Only team members can reference `@all`
|
||||
if node.has_attribute?('data-project')
|
||||
project = Project.find(node.attr('data-project')) rescue nil
|
||||
return false unless project
|
||||
|
||||
user && project.team.member?(user)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def call
|
||||
return doc if project.nil?
|
||||
|
||||
|
@ -114,9 +79,12 @@ module Banzai
|
|||
|
||||
def link_to_all(link_text: nil)
|
||||
project = context[:project]
|
||||
author = context[:author]
|
||||
|
||||
url = urls.namespace_project_url(project.namespace, project,
|
||||
only_path: context[:only_path])
|
||||
data = data_attribute(project: project.id)
|
||||
|
||||
data = data_attribute(project: project.id, author: author.try(:id))
|
||||
text = link_text || User.reference_prefix + 'all'
|
||||
|
||||
link_tag(url, data, text)
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
module Banzai
|
||||
class LazyReference
|
||||
def self.load(refs)
|
||||
lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
|
||||
|
||||
lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
|
||||
ids = refs.flat_map(&:ids)
|
||||
klass.where(id: ids)
|
||||
end
|
||||
|
||||
values + lazy_values
|
||||
end
|
||||
|
||||
attr_reader :klass, :ids
|
||||
|
||||
def initialize(klass, ids)
|
||||
@klass = klass
|
||||
@ids = Array.wrap(ids).map(&:to_i)
|
||||
end
|
||||
|
||||
def load
|
||||
self.klass.where(id: self.ids)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
module Banzai
|
||||
module Pipeline
|
||||
class ReferenceExtractionPipeline < BasePipeline
|
||||
def self.filters
|
||||
FilterArray[
|
||||
Filter::ReferenceGathererFilter
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +1,6 @@
|
|||
module Banzai
|
||||
# Extract possible GFM references from an arbitrary String for further processing.
|
||||
class ReferenceExtractor
|
||||
class << self
|
||||
LAZY_KEY = :banzai_reference_extractor_lazy
|
||||
|
||||
def lazy?
|
||||
Thread.current[LAZY_KEY]
|
||||
end
|
||||
|
||||
def lazily(values = nil, &block)
|
||||
return (values || block.call).uniq if lazy?
|
||||
|
||||
begin
|
||||
Thread.current[LAZY_KEY] = true
|
||||
|
||||
values ||= block.call
|
||||
|
||||
Banzai::LazyReference.load(values.uniq).uniq
|
||||
ensure
|
||||
Thread.current[LAZY_KEY] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@texts = []
|
||||
end
|
||||
|
@ -31,23 +9,21 @@ module Banzai
|
|||
@texts << Renderer.render(text, context)
|
||||
end
|
||||
|
||||
def references(type, context = {})
|
||||
filter = Banzai::Filter["#{type}_reference"]
|
||||
def references(type, project, current_user = nil)
|
||||
processor = Banzai::ReferenceParser[type].
|
||||
new(project, current_user)
|
||||
|
||||
context.merge!(
|
||||
pipeline: :reference_extraction,
|
||||
processor.process(html_documents)
|
||||
end
|
||||
|
||||
# ReferenceGathererFilter
|
||||
reference_filter: filter
|
||||
)
|
||||
private
|
||||
|
||||
self.class.lazily do
|
||||
@texts.flat_map do |html|
|
||||
text_context = context.dup
|
||||
result = Renderer.render_result(html, text_context)
|
||||
result[:references][type]
|
||||
end.uniq
|
||||
end
|
||||
def html_documents
|
||||
# This ensures that we don't memoize anything until we have a number of
|
||||
# text blobs to parse.
|
||||
return [] if @texts.empty?
|
||||
|
||||
@html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
14
lib/banzai/reference_parser.rb
Normal file
14
lib/banzai/reference_parser.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
# Returns the reference parser class for the given type
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Banzai::ReferenceParser['issue']
|
||||
#
|
||||
# This would return the `Banzai::ReferenceParser::IssueParser` class.
|
||||
def self.[](name)
|
||||
const_get("#{name.to_s.camelize}Parser")
|
||||
end
|
||||
end
|
||||
end
|
204
lib/banzai/reference_parser/base_parser.rb
Normal file
204
lib/banzai/reference_parser/base_parser.rb
Normal file
|
@ -0,0 +1,204 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
# Base class for reference parsing classes.
|
||||
#
|
||||
# Each parser should also specify its reference type by calling
|
||||
# `self.reference_type = ...` in the body of the class. The value of this
|
||||
# method should be a symbol such as `:issue` or `:merge_request`. For
|
||||
# example:
|
||||
#
|
||||
# class IssueParser < BaseParser
|
||||
# self.reference_type = :issue
|
||||
# end
|
||||
#
|
||||
# The reference type is used to determine what nodes to pass to the
|
||||
# `referenced_by` method.
|
||||
#
|
||||
# Parser classes should either implement the instance method
|
||||
# `references_relation` or overwrite `referenced_by`. The
|
||||
# `references_relation` method is supposed to return an
|
||||
# ActiveRecord::Relation used as a base relation for retrieving the objects
|
||||
# referenced in a set of HTML nodes.
|
||||
#
|
||||
# Each class can implement two additional methods:
|
||||
#
|
||||
# * `nodes_user_can_reference`: returns an Array of nodes the given user can
|
||||
# refer to.
|
||||
# * `nodes_visible_to_user`: returns an Array of nodes that are visible to
|
||||
# the given user.
|
||||
#
|
||||
# You only need to overwrite these methods if you want to tweak who can see
|
||||
# which references. For example, the IssueParser class defines its own
|
||||
# `nodes_visible_to_user` method so it can ensure users can only see issues
|
||||
# they have access to.
|
||||
class BaseParser
|
||||
class << self
|
||||
attr_accessor :reference_type
|
||||
end
|
||||
|
||||
# Returns the attribute name containing the value for every object to be
|
||||
# parsed by the current parser.
|
||||
#
|
||||
# For example, for a parser class that returns "Animal" objects this
|
||||
# attribute would be "data-animal".
|
||||
def self.data_attribute
|
||||
@data_attribute ||= "data-#{reference_type.to_s.dasherize}"
|
||||
end
|
||||
|
||||
def initialize(project = nil, current_user = nil)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
# Returns all the nodes containing references that the user can refer to.
|
||||
def nodes_user_can_reference(user, nodes)
|
||||
nodes
|
||||
end
|
||||
|
||||
# Returns all the nodes that are visible to the given user.
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
projects = lazy { projects_for_nodes(nodes) }
|
||||
project_attr = 'data-project'
|
||||
|
||||
nodes.select do |node|
|
||||
if node.has_attribute?(project_attr)
|
||||
node_id = node.attr(project_attr).to_i
|
||||
|
||||
if project && project.id == node_id
|
||||
true
|
||||
else
|
||||
can?(user, :read_project, projects[node_id])
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array of objects referenced by any of the given HTML nodes.
|
||||
def referenced_by(nodes)
|
||||
ids = unique_attribute_values(nodes, self.class.data_attribute)
|
||||
|
||||
references_relation.where(id: ids)
|
||||
end
|
||||
|
||||
# Returns the ActiveRecord::Relation to use for querying references in the
|
||||
# DB.
|
||||
def references_relation
|
||||
raise NotImplementedError,
|
||||
"#{self.class} does not implement #{__method__}"
|
||||
end
|
||||
|
||||
# Returns a Hash containing attribute values per project ID.
|
||||
#
|
||||
# The returned Hash uses the following format:
|
||||
#
|
||||
# { project id => [value1, value2, ...] }
|
||||
#
|
||||
# nodes - An Array of HTML nodes to process.
|
||||
# attribute - The name of the attribute (as a String) for which to gather
|
||||
# values.
|
||||
#
|
||||
# Returns a Hash.
|
||||
def gather_attributes_per_project(nodes, attribute)
|
||||
per_project = Hash.new { |hash, key| hash[key] = Set.new }
|
||||
|
||||
nodes.each do |node|
|
||||
project_id = node.attr('data-project').to_i
|
||||
id = node.attr(attribute)
|
||||
|
||||
per_project[project_id] << id if id
|
||||
end
|
||||
|
||||
per_project
|
||||
end
|
||||
|
||||
# Returns a Hash containing objects for an attribute grouped per their
|
||||
# IDs.
|
||||
#
|
||||
# The returned Hash uses the following format:
|
||||
#
|
||||
# { id value => row }
|
||||
#
|
||||
# nodes - An Array of HTML nodes to process.
|
||||
#
|
||||
# collection - The model or ActiveRecord relation to use for retrieving
|
||||
# rows from the database.
|
||||
#
|
||||
# attribute - The name of the attribute containing the primary key values
|
||||
# for every row.
|
||||
#
|
||||
# Returns a Hash.
|
||||
def grouped_objects_for_nodes(nodes, collection, attribute)
|
||||
return {} if nodes.empty?
|
||||
|
||||
ids = unique_attribute_values(nodes, attribute)
|
||||
|
||||
collection.where(id: ids).each_with_object({}) do |row, hash|
|
||||
hash[row.id] = row
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an Array containing all unique values of an attribute of the
|
||||
# given nodes.
|
||||
def unique_attribute_values(nodes, attribute)
|
||||
values = Set.new
|
||||
|
||||
nodes.each do |node|
|
||||
if node.has_attribute?(attribute)
|
||||
values << node.attr(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
values.to_a
|
||||
end
|
||||
|
||||
# Processes the list of HTML documents and returns an Array containing all
|
||||
# the references.
|
||||
def process(documents)
|
||||
type = self.class.reference_type
|
||||
|
||||
nodes = documents.flat_map do |document|
|
||||
Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
|
||||
end
|
||||
|
||||
gather_references(nodes)
|
||||
end
|
||||
|
||||
# Gathers the references for the given HTML nodes.
|
||||
def gather_references(nodes)
|
||||
nodes = nodes_user_can_reference(current_user, nodes)
|
||||
nodes = nodes_visible_to_user(current_user, nodes)
|
||||
|
||||
referenced_by(nodes)
|
||||
end
|
||||
|
||||
# Returns a Hash containing the projects for a given list of HTML nodes.
|
||||
#
|
||||
# The returned Hash uses the following format:
|
||||
#
|
||||
# { project ID => project }
|
||||
#
|
||||
def projects_for_nodes(nodes)
|
||||
@projects_for_nodes ||=
|
||||
grouped_objects_for_nodes(nodes, Project, 'data-project')
|
||||
end
|
||||
|
||||
def can?(user, permission, subject)
|
||||
Ability.abilities.allowed?(user, permission, subject)
|
||||
end
|
||||
|
||||
def find_projects_for_hash_keys(hash)
|
||||
Project.where(id: hash.keys)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user, :project
|
||||
|
||||
def lazy(&block)
|
||||
Gitlab::Lazy.new(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
lib/banzai/reference_parser/commit_parser.rb
Normal file
34
lib/banzai/reference_parser/commit_parser.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class CommitParser < BaseParser
|
||||
self.reference_type = :commit
|
||||
|
||||
def referenced_by(nodes)
|
||||
commit_ids = commit_ids_per_project(nodes)
|
||||
projects = find_projects_for_hash_keys(commit_ids)
|
||||
|
||||
projects.flat_map do |project|
|
||||
find_commits(project, commit_ids[project.id])
|
||||
end
|
||||
end
|
||||
|
||||
def commit_ids_per_project(nodes)
|
||||
gather_attributes_per_project(nodes, self.class.data_attribute)
|
||||
end
|
||||
|
||||
def find_commits(project, ids)
|
||||
commits = []
|
||||
|
||||
return commits unless project.valid_repo?
|
||||
|
||||
ids.each do |id|
|
||||
commit = project.commit(id)
|
||||
|
||||
commits << commit if commit
|
||||
end
|
||||
|
||||
commits
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
38
lib/banzai/reference_parser/commit_range_parser.rb
Normal file
38
lib/banzai/reference_parser/commit_range_parser.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class CommitRangeParser < BaseParser
|
||||
self.reference_type = :commit_range
|
||||
|
||||
def referenced_by(nodes)
|
||||
range_ids = commit_range_ids_per_project(nodes)
|
||||
projects = find_projects_for_hash_keys(range_ids)
|
||||
|
||||
projects.flat_map do |project|
|
||||
find_ranges(project, range_ids[project.id])
|
||||
end
|
||||
end
|
||||
|
||||
def commit_range_ids_per_project(nodes)
|
||||
gather_attributes_per_project(nodes, self.class.data_attribute)
|
||||
end
|
||||
|
||||
def find_ranges(project, range_ids)
|
||||
ranges = []
|
||||
|
||||
range_ids.each do |id|
|
||||
range = find_object(project, id)
|
||||
|
||||
ranges << range if range
|
||||
end
|
||||
|
||||
ranges
|
||||
end
|
||||
|
||||
def find_object(project, id)
|
||||
range = CommitRange.new(id, project)
|
||||
|
||||
range.valid_commits? ? range : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
lib/banzai/reference_parser/external_issue_parser.rb
Normal file
25
lib/banzai/reference_parser/external_issue_parser.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class ExternalIssueParser < BaseParser
|
||||
self.reference_type = :external_issue
|
||||
|
||||
def referenced_by(nodes)
|
||||
issue_ids = issue_ids_per_project(nodes)
|
||||
projects = find_projects_for_hash_keys(issue_ids)
|
||||
issues = []
|
||||
|
||||
projects.each do |project|
|
||||
issue_ids[project.id].each do |id|
|
||||
issues << ExternalIssue.new(id, project)
|
||||
end
|
||||
end
|
||||
|
||||
issues
|
||||
end
|
||||
|
||||
def issue_ids_per_project(nodes)
|
||||
gather_attributes_per_project(nodes, self.class.data_attribute)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
40
lib/banzai/reference_parser/issue_parser.rb
Normal file
40
lib/banzai/reference_parser/issue_parser.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class IssueParser < BaseParser
|
||||
self.reference_type = :issue
|
||||
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
# It is not possible to check access rights for external issue trackers
|
||||
return nodes if project && project.external_issue_tracker
|
||||
|
||||
issues = issues_for_nodes(nodes)
|
||||
|
||||
nodes.select do |node|
|
||||
issue = issue_for_node(issues, node)
|
||||
|
||||
issue ? can?(user, :read_issue, issue) : false
|
||||
end
|
||||
end
|
||||
|
||||
def referenced_by(nodes)
|
||||
issues = issues_for_nodes(nodes)
|
||||
|
||||
nodes.map { |node| issue_for_node(issues, node) }.uniq
|
||||
end
|
||||
|
||||
def issues_for_nodes(nodes)
|
||||
@issues_for_nodes ||= grouped_objects_for_nodes(
|
||||
nodes,
|
||||
Issue.all.includes(:author, :assignee, :project),
|
||||
self.class.data_attribute
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issue_for_node(issues, node)
|
||||
issues[node.attr(self.class.data_attribute).to_i]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/banzai/reference_parser/label_parser.rb
Normal file
11
lib/banzai/reference_parser/label_parser.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class LabelParser < BaseParser
|
||||
self.reference_type = :label
|
||||
|
||||
def references_relation
|
||||
Label
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/banzai/reference_parser/merge_request_parser.rb
Normal file
11
lib/banzai/reference_parser/merge_request_parser.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class MergeRequestParser < BaseParser
|
||||
self.reference_type = :merge_request
|
||||
|
||||
def references_relation
|
||||
MergeRequest.includes(:author, :assignee, :target_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/banzai/reference_parser/milestone_parser.rb
Normal file
11
lib/banzai/reference_parser/milestone_parser.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class MilestoneParser < BaseParser
|
||||
self.reference_type = :milestone
|
||||
|
||||
def references_relation
|
||||
Milestone
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/banzai/reference_parser/snippet_parser.rb
Normal file
11
lib/banzai/reference_parser/snippet_parser.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class SnippetParser < BaseParser
|
||||
self.reference_type = :snippet
|
||||
|
||||
def references_relation
|
||||
Snippet
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
92
lib/banzai/reference_parser/user_parser.rb
Normal file
92
lib/banzai/reference_parser/user_parser.rb
Normal file
|
@ -0,0 +1,92 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class UserParser < BaseParser
|
||||
self.reference_type = :user
|
||||
|
||||
def referenced_by(nodes)
|
||||
group_ids = []
|
||||
user_ids = []
|
||||
project_ids = []
|
||||
|
||||
nodes.each do |node|
|
||||
if node.has_attribute?('data-group')
|
||||
group_ids << node.attr('data-group').to_i
|
||||
elsif node.has_attribute?(self.class.data_attribute)
|
||||
user_ids << node.attr(self.class.data_attribute).to_i
|
||||
elsif node.has_attribute?('data-project')
|
||||
project_ids << node.attr('data-project').to_i
|
||||
end
|
||||
end
|
||||
|
||||
find_users_for_groups(group_ids) | find_users(user_ids) |
|
||||
find_users_for_projects(project_ids)
|
||||
end
|
||||
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
group_attr = 'data-group'
|
||||
groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) }
|
||||
visible = []
|
||||
remaining = []
|
||||
|
||||
nodes.each do |node|
|
||||
if node.has_attribute?(group_attr)
|
||||
node_group = groups[node.attr(group_attr).to_i]
|
||||
|
||||
if node_group &&
|
||||
can?(user, :read_group, node_group)
|
||||
visible << node
|
||||
end
|
||||
# Remaining nodes will be processed by the parent class'
|
||||
# implementation of this method.
|
||||
else
|
||||
remaining << node
|
||||
end
|
||||
end
|
||||
|
||||
visible + super(current_user, remaining)
|
||||
end
|
||||
|
||||
def nodes_user_can_reference(current_user, nodes)
|
||||
project_attr = 'data-project'
|
||||
author_attr = 'data-author'
|
||||
|
||||
projects = lazy { projects_for_nodes(nodes) }
|
||||
users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) }
|
||||
|
||||
nodes.select do |node|
|
||||
project_id = node.attr(project_attr)
|
||||
user_id = node.attr(author_attr)
|
||||
|
||||
if project && project_id && project.id == project_id.to_i
|
||||
true
|
||||
elsif project_id && user_id
|
||||
project = projects[project_id.to_i]
|
||||
user = users[user_id.to_i]
|
||||
|
||||
project && user ? project.team.member?(user) : false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_users(ids)
|
||||
return [] if ids.empty?
|
||||
|
||||
User.where(id: ids).to_a
|
||||
end
|
||||
|
||||
def find_users_for_groups(ids)
|
||||
return [] if ids.empty?
|
||||
|
||||
User.joins(:group_members).where(members: { source_id: ids }).to_a
|
||||
end
|
||||
|
||||
def find_users_for_projects(ids)
|
||||
return [] if ids.empty?
|
||||
|
||||
Project.where(id: ids).flat_map { |p| p.team.members.to_a }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
lib/gitlab/lazy.rb
Normal file
34
lib/gitlab/lazy.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
module Gitlab
|
||||
# A class that can be wrapped around an expensive method call so it's only
|
||||
# executed when actually needed.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# object = Gitlab::Lazy.new { some_expensive_work_here }
|
||||
#
|
||||
# object['foo']
|
||||
# object.bar
|
||||
class Lazy < BasicObject
|
||||
def initialize(&block)
|
||||
@block = block
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
__evaluate__
|
||||
|
||||
@result.__send__(name, *args, &block)
|
||||
end
|
||||
|
||||
def respond_to_missing?(name, include_private = false)
|
||||
__evaluate__
|
||||
|
||||
@result.respond_to?(name, include_private) || super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def __evaluate__
|
||||
@result = @block.call unless defined?(@result)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,10 +4,9 @@ module Gitlab
|
|||
REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
|
||||
attr_accessor :project, :current_user, :author
|
||||
|
||||
def initialize(project, current_user = nil, author = nil)
|
||||
def initialize(project, current_user = nil)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@author = author
|
||||
|
||||
@references = {}
|
||||
|
||||
|
@ -18,17 +17,21 @@ module Gitlab
|
|||
super(text, context.merge(project: project))
|
||||
end
|
||||
|
||||
def references(type)
|
||||
super(type, project, current_user)
|
||||
end
|
||||
|
||||
REFERABLES.each do |type|
|
||||
define_method("#{type}s") do
|
||||
@references[type] ||= references(type, reference_context)
|
||||
@references[type] ||= references(type)
|
||||
end
|
||||
end
|
||||
|
||||
def issues
|
||||
if project && project.jira_tracker?
|
||||
@references[:external_issue] ||= references(:external_issue, reference_context)
|
||||
@references[:external_issue] ||= references(:external_issue)
|
||||
else
|
||||
@references[:issue] ||= references(:issue, reference_context)
|
||||
@references[:issue] ||= references(:issue)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -46,11 +49,5 @@ module Gitlab
|
|||
|
||||
@pattern = Regexp.union(patterns.compact)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reference_context
|
||||
{ project: project, current_user: current_user, author: author }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
|
|||
expect(link).not_to match %r(https?://)
|
||||
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit_range]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project reference' do
|
||||
|
@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
|
|||
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit_range]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL reference' do
|
||||
|
@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
|
|||
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit_range]).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
|
|||
expect(link).not_to match %r(https?://)
|
||||
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project reference' do
|
||||
|
@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
|
|||
exp = act = "Committed #{invalidate_reference(reference)}"
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL reference' do
|
||||
|
@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
|
|||
act = "Committed #{invalidate_reference(reference)}"
|
||||
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("See #{reference}")
|
||||
expect(result[:references][:commit]).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
|||
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
end
|
||||
|
||||
it 'does not process links containing issue numbers followed by text' do
|
||||
href = "#{reference}st"
|
||||
doc = reference_filter("<a href='#{href}'></a>")
|
||||
|
@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
|||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL reference' do
|
||||
|
@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
|||
doc = reference_filter("Fixed (#{reference}.)")
|
||||
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project reference in link href' do
|
||||
|
@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
|||
doc = reference_filter("Fixed (#{reference}.)")
|
||||
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL in link href' do
|
||||
|
@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
|
|||
doc = reference_filter("Fixed (#{reference}.)")
|
||||
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Fixed #{reference}")
|
||||
expect(result[:references][:issue]).to eq [issue]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
|
|||
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Label #{reference}")
|
||||
expect(result[:references][:label]).to eq [label]
|
||||
end
|
||||
|
||||
describe 'label span element' do
|
||||
it 'includes default classes' do
|
||||
doc = reference_filter("Label #{reference}")
|
||||
|
@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
|
|||
expect(link).to have_attribute('data-label')
|
||||
expect(link.attr('data-label')).to eq label.id.to_s
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Label #{reference}")
|
||||
expect(result[:references][:label]).to eq [label]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'cross project label references' do
|
||||
|
|
|
@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
|
|||
expect(link).not_to match %r(https?://)
|
||||
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Merge #{reference}")
|
||||
expect(result[:references][:merge_request]).to eq [merge]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project reference' do
|
||||
|
@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
|
|||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Merge #{reference}")
|
||||
expect(result[:references][:merge_request]).to eq [merge]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL reference' do
|
||||
|
@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
|
|||
doc = reference_filter("Merge (#{reference}.)")
|
||||
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Merge #{reference}")
|
||||
expect(result[:references][:merge_request]).to eq [merge]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,11 +48,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
|
|||
namespace_project_milestone_path(project.namespace, project, milestone)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Milestone #{reference}")
|
||||
expect(result[:references][:milestone]).to eq [milestone]
|
||||
end
|
||||
|
||||
context 'Integer-based references' do
|
||||
it 'links to a valid reference' do
|
||||
doc = reference_filter("See #{reference}")
|
||||
|
@ -151,11 +146,6 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
|
|||
expect(link).to have_attribute('data-milestone')
|
||||
expect(link.attr('data-milestone')).to eq milestone.id.to_s
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Milestone #{reference}")
|
||||
expect(result[:references][:milestone]).to eq [milestone]
|
||||
end
|
||||
end
|
||||
|
||||
describe 'cross project milestone references' do
|
||||
|
|
|
@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
end
|
||||
|
||||
context 'with data-project' do
|
||||
let(:parser_class) do
|
||||
Class.new(Banzai::ReferenceParser::BaseParser) do
|
||||
self.reference_type = :test
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Banzai::ReferenceParser).to receive(:[]).
|
||||
with('test').
|
||||
and_return(parser_class)
|
||||
end
|
||||
|
||||
it 'removes unpermitted Project references' do
|
||||
user = create(:user)
|
||||
project = create(:empty_project)
|
||||
|
||||
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
|
||||
link = reference_link(project: project.id, reference_type: 'test')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project)
|
||||
project.team << [user, :master]
|
||||
|
||||
link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
|
||||
link = reference_link(project: project.id, reference_type: 'test')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
end
|
||||
|
||||
it 'handles invalid Project references' do
|
||||
link = reference_link(project: 12345, reference_filter: 'ReferenceFilter')
|
||||
link = reference_link(project: 12345, reference_type: 'test')
|
||||
|
||||
expect { filter(link) }.not_to raise_error
|
||||
end
|
||||
|
@ -51,7 +63,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project, :public)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: non_member)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -62,7 +74,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project, :public)
|
||||
issue = create(:issue, :confidential, project: project, author: author)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: author)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -73,7 +85,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project, :public)
|
||||
issue = create(:issue, :confidential, project: project, assignee: assignee)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: assignee)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -85,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project.team << [member, :developer]
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: member)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -96,7 +108,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project, :public)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: admin)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -108,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
project = create(:empty_project, :public)
|
||||
issue = create(:issue, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -121,7 +133,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
user = create(:user)
|
||||
group = create(:group, :private)
|
||||
|
||||
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
|
||||
link = reference_link(group: group.id, reference_type: 'user')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -132,14 +144,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
group = create(:group, :private)
|
||||
group.add_developer(user)
|
||||
|
||||
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
|
||||
link = reference_link(group: group.id, reference_type: 'user')
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
end
|
||||
|
||||
it 'handles invalid Group references' do
|
||||
link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
|
||||
link = reference_link(group: 12345, reference_type: 'user')
|
||||
|
||||
expect { filter(link) }.not_to raise_error
|
||||
end
|
||||
|
@ -149,7 +161,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
|
|||
it 'allows any User reference' do
|
||||
user = create(:user)
|
||||
|
||||
link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
|
||||
link = reference_link(user: user.id, reference_type: 'user')
|
||||
doc = filter(link)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::Filter::ReferenceGathererFilter, lib: true do
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include FilterSpecHelper
|
||||
|
||||
def reference_link(data)
|
||||
link_to('text', '', class: 'gfm', data: data)
|
||||
end
|
||||
|
||||
context "for issue references" do
|
||||
|
||||
context 'with data-project' do
|
||||
it 'removes unpermitted Project references' do
|
||||
user = create(:user)
|
||||
project = create(:empty_project)
|
||||
issue = create(:issue, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
result = pipeline_result(link, current_user: user)
|
||||
|
||||
expect(result[:references][:issue]).to be_empty
|
||||
end
|
||||
|
||||
it 'allows permitted Project references' do
|
||||
user = create(:user)
|
||||
project = create(:empty_project)
|
||||
issue = create(:issue, project: project)
|
||||
project.team << [user, :master]
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
|
||||
result = pipeline_result(link, current_user: user)
|
||||
|
||||
expect(result[:references][:issue]).to eq([issue])
|
||||
end
|
||||
|
||||
it 'handles invalid Project references' do
|
||||
link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter')
|
||||
|
||||
expect { pipeline_result(link) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for user references" do
|
||||
|
||||
context 'with data-group' do
|
||||
it 'removes unpermitted Group references' do
|
||||
user = create(:user)
|
||||
group = create(:group)
|
||||
|
||||
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
|
||||
result = pipeline_result(link, current_user: user)
|
||||
|
||||
expect(result[:references][:user]).to be_empty
|
||||
end
|
||||
|
||||
it 'allows permitted Group references' do
|
||||
user = create(:user)
|
||||
group = create(:group)
|
||||
group.add_developer(user)
|
||||
|
||||
link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
|
||||
result = pipeline_result(link, current_user: user)
|
||||
|
||||
expect(result[:references][:user]).to eq([user])
|
||||
end
|
||||
|
||||
it 'handles invalid Group references' do
|
||||
link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
|
||||
|
||||
expect { pipeline_result(link) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'with data-user' do
|
||||
it 'allows any User reference' do
|
||||
user = create(:user)
|
||||
|
||||
link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
|
||||
result = pipeline_result(link)
|
||||
|
||||
expect(result[:references][:user]).to eq([user])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
|
|||
expect(link).not_to match %r(https?://)
|
||||
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Snippet #{reference}")
|
||||
expect(result[:references][:snippet]).to eq [snippet]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project reference' do
|
||||
|
@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
|
|||
|
||||
expect(reference_filter(act).to_html).to eq exp
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Snippet #{reference}")
|
||||
expect(result[:references][:snippet]).to eq [snippet]
|
||||
end
|
||||
end
|
||||
|
||||
context 'cross-project URL reference' do
|
||||
|
@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
|
|||
|
||||
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Snippet #{reference}")
|
||||
expect(result[:references][:snippet]).to eq [snippet]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
|
|||
end
|
||||
|
||||
it 'supports a special @all mention' do
|
||||
doc = reference_filter("Hey #{reference}")
|
||||
doc = reference_filter("Hey #{reference}", author: user)
|
||||
expect(doc.css('a').length).to eq 1
|
||||
expect(doc.css('a').first.attr('href'))
|
||||
.to eq urls.namespace_project_url(project.namespace, project)
|
||||
end
|
||||
|
||||
context "when the author is a member of the project" do
|
||||
it 'includes a data-author attribute when there is an author' do
|
||||
doc = reference_filter(reference, author: user)
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Hey #{reference}", author: project.creator)
|
||||
expect(result[:references][:user]).to eq [project.creator]
|
||||
end
|
||||
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
|
||||
end
|
||||
|
||||
context "when the author is not a member of the project" do
|
||||
it 'does not include a data-author attribute when there is no author' do
|
||||
doc = reference_filter(reference)
|
||||
|
||||
let(:other_user) { create(:user) }
|
||||
|
||||
it "doesn't add to the results hash" do
|
||||
result = reference_pipeline_result("Hey #{reference}", author: other_user)
|
||||
expect(result[:references][:user]).to eq []
|
||||
end
|
||||
expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
|
|||
expect(link).to have_attribute('data-user')
|
||||
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Hey #{reference}")
|
||||
expect(result[:references][:user]).to eq [user]
|
||||
end
|
||||
end
|
||||
|
||||
context 'mentioning a group' do
|
||||
|
@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
|
|||
expect(link).to have_attribute('data-group')
|
||||
expect(link.attr('data-group')).to eq group.id.to_s
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Hey #{reference}")
|
||||
expect(result[:references][:user]).to eq group.users
|
||||
end
|
||||
end
|
||||
|
||||
it 'links with adjacent text' do
|
||||
|
@ -151,10 +135,5 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
|
|||
expect(link).to have_attribute('data-user')
|
||||
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
|
||||
end
|
||||
|
||||
it 'adds to the results hash' do
|
||||
result = reference_pipeline_result("Hey #{reference}")
|
||||
expect(result[:references][:user]).to eq [user]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
237
spec/lib/banzai/reference_parser/base_parser_spec.rb
Normal file
237
spec/lib/banzai/reference_parser/base_parser_spec.rb
Normal file
|
@ -0,0 +1,237 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::BaseParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
|
||||
subject do
|
||||
klass = Class.new(described_class) do
|
||||
self.reference_type = :foo
|
||||
end
|
||||
|
||||
klass.new(project, user)
|
||||
end
|
||||
|
||||
describe '.reference_type=' do
|
||||
it 'sets the reference type' do
|
||||
dummy = Class.new(described_class)
|
||||
dummy.reference_type = :foo
|
||||
|
||||
expect(dummy.reference_type).to eq(:foo)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#nodes_visible_to_user' do
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
context 'when the link has a data-project attribute' do
|
||||
it 'returns the nodes if the attribute value equals the current project ID' do
|
||||
link['data-project'] = project.id.to_s
|
||||
|
||||
expect(Ability.abilities).not_to receive(:allowed?)
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns the nodes if the user can read the project' do
|
||||
other_project = create(:empty_project, :public)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_project, other_project).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the attribute value is empty' do
|
||||
link['data-project'] = ''
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user can not read the project' do
|
||||
other_project = create(:empty_project, :public)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_project, other_project).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-project attribute' do
|
||||
it 'returns the nodes' do
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#nodes_user_can_reference' do
|
||||
it 'returns the nodes' do
|
||||
link = double(:link)
|
||||
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when references_relation is implemented' do
|
||||
it 'returns a collection of objects' do
|
||||
links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>").
|
||||
children
|
||||
|
||||
expect(subject).to receive(:references_relation).and_return(User)
|
||||
expect(subject.referenced_by(links)).to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when references_relation is not implemented' do
|
||||
it 'raises NotImplementedError' do
|
||||
links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children
|
||||
|
||||
expect { subject.referenced_by(links) }.
|
||||
to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#references_relation' do
|
||||
it 'raises NotImplementedError' do
|
||||
expect { subject.references_relation }.to raise_error(NotImplementedError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gather_attributes_per_project' do
|
||||
it 'returns a Hash containing attribute values per project' do
|
||||
link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>').
|
||||
children[0]
|
||||
|
||||
hash = subject.gather_attributes_per_project([link], 'data-foo')
|
||||
|
||||
expect(hash).to be_an_instance_of(Hash)
|
||||
|
||||
expect(hash[1].to_a).to eq(['2'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#grouped_objects_for_nodes' do
|
||||
it 'returns a Hash grouping objects per ID' do
|
||||
nodes = [double(:node)]
|
||||
|
||||
expect(subject).to receive(:unique_attribute_values).
|
||||
with(nodes, 'data-user').
|
||||
and_return([user.id])
|
||||
|
||||
hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
|
||||
|
||||
expect(hash).to eq({ user.id => user })
|
||||
end
|
||||
|
||||
it 'returns an empty Hash when the list of nodes is empty' do
|
||||
expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unique_attribute_values' do
|
||||
it 'returns an Array of unique values' do
|
||||
link = double(:link)
|
||||
|
||||
expect(link).to receive(:has_attribute?).
|
||||
with('data-foo').
|
||||
twice.
|
||||
and_return(true)
|
||||
|
||||
expect(link).to receive(:attr).
|
||||
with('data-foo').
|
||||
twice.
|
||||
and_return('1')
|
||||
|
||||
nodes = [link, link]
|
||||
|
||||
expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1'])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#process' do
|
||||
it 'gathers the references for every node matching the reference type' do
|
||||
dummy = Class.new(described_class) do
|
||||
self.reference_type = :test
|
||||
end
|
||||
|
||||
instance = dummy.new(project, user)
|
||||
document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
|
||||
|
||||
expect(instance).to receive(:gather_references).
|
||||
with([document.children[1]]).
|
||||
and_return([user])
|
||||
|
||||
expect(instance.process([document])).to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gather_references' do
|
||||
let(:link) { double(:link) }
|
||||
|
||||
it 'does not process links a user can not reference' do
|
||||
expect(subject).to receive(:nodes_user_can_reference).
|
||||
with(user, [link]).
|
||||
and_return([])
|
||||
|
||||
expect(subject).to receive(:referenced_by).with([])
|
||||
|
||||
subject.gather_references([link])
|
||||
end
|
||||
|
||||
it 'does not process links a user can not see' do
|
||||
expect(subject).to receive(:nodes_user_can_reference).
|
||||
with(user, [link]).
|
||||
and_return([link])
|
||||
|
||||
expect(subject).to receive(:nodes_visible_to_user).
|
||||
with(user, [link]).
|
||||
and_return([])
|
||||
|
||||
expect(subject).to receive(:referenced_by).with([])
|
||||
|
||||
subject.gather_references([link])
|
||||
end
|
||||
|
||||
it 'returns the references if a user can reference and see a link' do
|
||||
expect(subject).to receive(:nodes_user_can_reference).
|
||||
with(user, [link]).
|
||||
and_return([link])
|
||||
|
||||
expect(subject).to receive(:nodes_visible_to_user).
|
||||
with(user, [link]).
|
||||
and_return([link])
|
||||
|
||||
expect(subject).to receive(:referenced_by).with([link])
|
||||
|
||||
subject.gather_references([link])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can?' do
|
||||
it 'delegates the permissions check to the Ability class' do
|
||||
user = double(:user)
|
||||
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_project, project)
|
||||
|
||||
subject.can?(user, :read_project, project)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_projects_for_hash_keys' do
|
||||
it 'returns a list of Projects' do
|
||||
expect(subject.find_projects_for_hash_keys(project.id => project)).
|
||||
to eq([project])
|
||||
end
|
||||
end
|
||||
end
|
113
spec/lib/banzai/reference_parser/commit_parser_spec.rb
Normal file
113
spec/lib/banzai/reference_parser/commit_parser_spec.rb
Normal file
|
@ -0,0 +1,113 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::CommitParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when the link has a data-project attribute' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
context 'when the link has a data-commit attribute' do
|
||||
before do
|
||||
link['data-commit'] = '123'
|
||||
end
|
||||
|
||||
it 'returns an Array of commits' do
|
||||
commit = double(:commit)
|
||||
|
||||
allow_any_instance_of(Project).to receive(:valid_repo?).
|
||||
and_return(true)
|
||||
|
||||
expect(subject).to receive(:find_commits).
|
||||
with(project, ['123']).
|
||||
and_return([commit])
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([commit])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the commit could not be found' do
|
||||
allow_any_instance_of(Project).to receive(:valid_repo?).
|
||||
and_return(true)
|
||||
|
||||
expect(subject).to receive(:find_commits).
|
||||
with(project, ['123']).
|
||||
and_return([])
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
|
||||
it 'skips projects without valid repositories' do
|
||||
allow_any_instance_of(Project).to receive(:valid_repo?).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-commit attribute' do
|
||||
it 'returns an empty Array' do
|
||||
allow_any_instance_of(Project).to receive(:valid_repo?).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-project attribute' do
|
||||
it 'returns an empty Array' do
|
||||
allow_any_instance_of(Project).to receive(:valid_repo?).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#commit_ids_per_project' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
it 'returns a Hash containing commit IDs per project' do
|
||||
link['data-commit'] = '123'
|
||||
|
||||
hash = subject.commit_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_an_instance_of(Hash)
|
||||
|
||||
expect(hash[project.id].to_a).to eq(['123'])
|
||||
end
|
||||
|
||||
it 'does not add a project when the data-commit attribute is empty' do
|
||||
hash = subject.commit_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_commits' do
|
||||
it 'returns an Array of commit objects' do
|
||||
commit = double(:commit)
|
||||
|
||||
expect(project).to receive(:commit).with('123').and_return(commit)
|
||||
expect(project).to receive(:valid_repo?).and_return(true)
|
||||
|
||||
expect(subject.find_commits(project, %w{123})).to eq([commit])
|
||||
end
|
||||
|
||||
it 'skips commit IDs for which no commit could be found' do
|
||||
expect(project).to receive(:commit).with('123').and_return(nil)
|
||||
expect(project).to receive(:valid_repo?).and_return(true)
|
||||
|
||||
expect(subject.find_commits(project, %w{123})).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
120
spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
Normal file
120
spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
Normal file
|
@ -0,0 +1,120 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when the link has a data-project attribute' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
context 'when the link as a data-commit-range attribute' do
|
||||
before do
|
||||
link['data-commit-range'] = '123..456'
|
||||
end
|
||||
|
||||
it 'returns an Array of commit ranges' do
|
||||
range = double(:range)
|
||||
|
||||
expect(subject).to receive(:find_object).
|
||||
with(project, '123..456').
|
||||
and_return(range)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([range])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the commit range could not be found' do
|
||||
expect(subject).to receive(:find_object).
|
||||
with(project, '123..456').
|
||||
and_return(nil)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-commit-range attribute' do
|
||||
it 'returns an empty Array' do
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-project attribute' do
|
||||
it 'returns an empty Array' do
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#commit_range_ids_per_project' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
it 'returns a Hash containing range IDs per project' do
|
||||
link['data-commit-range'] = '123..456'
|
||||
|
||||
hash = subject.commit_range_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_an_instance_of(Hash)
|
||||
|
||||
expect(hash[project.id].to_a).to eq(['123..456'])
|
||||
end
|
||||
|
||||
it 'does not add a project when the data-commit-range attribute is empty' do
|
||||
hash = subject.commit_range_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_ranges' do
|
||||
it 'returns an Array of range objects' do
|
||||
range = double(:commit)
|
||||
|
||||
expect(subject).to receive(:find_object).
|
||||
with(project, '123..456').
|
||||
and_return(range)
|
||||
|
||||
expect(subject.find_ranges(project, ['123..456'])).to eq([range])
|
||||
end
|
||||
|
||||
it 'skips ranges that could not be found' do
|
||||
expect(subject).to receive(:find_object).
|
||||
with(project, '123..456').
|
||||
and_return(nil)
|
||||
|
||||
expect(subject.find_ranges(project, ['123..456'])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_object' do
|
||||
let(:range) { double(:range) }
|
||||
|
||||
before do
|
||||
expect(CommitRange).to receive(:new).and_return(range)
|
||||
end
|
||||
|
||||
context 'when the range has valid commits' do
|
||||
it 'returns the commit range' do
|
||||
expect(range).to receive(:valid_commits?).and_return(true)
|
||||
|
||||
expect(subject.find_object(project, '123..456')).to eq(range)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the range does not have any valid commits' do
|
||||
it 'returns nil' do
|
||||
expect(range).to receive(:valid_commits?).and_return(false)
|
||||
|
||||
expect(subject.find_object(project, '123..456')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when the link has a data-project attribute' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
context 'when the link has a data-external-issue attribute' do
|
||||
it 'returns an Array of ExternalIssue instances' do
|
||||
link['data-external-issue'] = '123'
|
||||
|
||||
refs = subject.referenced_by([link])
|
||||
|
||||
expect(refs).to eq([ExternalIssue.new('123', project)])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-external-issue attribute' do
|
||||
it 'returns an empty Array' do
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-project attribute' do
|
||||
it 'returns an empty Array' do
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_ids_per_project' do
|
||||
before do
|
||||
link['data-project'] = project.id.to_s
|
||||
end
|
||||
|
||||
it 'returns a Hash containing range IDs per project' do
|
||||
link['data-external-issue'] = '123'
|
||||
|
||||
hash = subject.issue_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_an_instance_of(Hash)
|
||||
|
||||
expect(hash[project.id].to_a).to eq(['123'])
|
||||
end
|
||||
|
||||
it 'does not add a project when the data-external-issue attribute is empty' do
|
||||
hash = subject.issue_ids_per_project([link])
|
||||
|
||||
expect(hash).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
79
spec/lib/banzai/reference_parser/issue_parser_spec.rb
Normal file
79
spec/lib/banzai/reference_parser/issue_parser_spec.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::IssueParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#nodes_visible_to_user' do
|
||||
context 'when the link has a data-issue attribute' do
|
||||
before do
|
||||
link['data-issue'] = issue.id.to_s
|
||||
end
|
||||
|
||||
it 'returns the nodes when the user can read the issue' do
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_issue, issue).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user can not read the issue' do
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_issue, issue).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-issue attribute' do
|
||||
it 'returns an empty Array' do
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project uses an external issue tracker' do
|
||||
it 'returns all nodes' do
|
||||
link = double(:link)
|
||||
|
||||
expect(project).to receive(:external_issue_tracker).and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when the link has a data-issue attribute' do
|
||||
context 'using an existing issue ID' do
|
||||
before do
|
||||
link['data-issue'] = issue.id.to_s
|
||||
end
|
||||
|
||||
it 'returns an Array of issues' do
|
||||
expect(subject.referenced_by([link])).to eq([issue])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the list of nodes is empty' do
|
||||
expect(subject.referenced_by([link])).to eq([issue])
|
||||
expect(subject.referenced_by([])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issues_for_nodes' do
|
||||
it 'returns a Hash containing the issues for a list of nodes' do
|
||||
link['data-issue'] = issue.id.to_s
|
||||
nodes = [link]
|
||||
|
||||
expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue })
|
||||
end
|
||||
end
|
||||
end
|
31
spec/lib/banzai/reference_parser/label_parser_spec.rb
Normal file
31
spec/lib/banzai/reference_parser/label_parser_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::LabelParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:label) { create(:label, project: project) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
describe 'when the link has a data-label attribute' do
|
||||
context 'using an existing label ID' do
|
||||
it 'returns an Array of labels' do
|
||||
link['data-label'] = label.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([label])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing label ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-label'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
subject { described_class.new(merge_request.target_project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
describe 'when the link has a data-merge-request attribute' do
|
||||
context 'using an existing merge request ID' do
|
||||
it 'returns an Array of merge requests' do
|
||||
link['data-merge-request'] = merge_request.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([merge_request])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing merge request ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-merge-request'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
spec/lib/banzai/reference_parser/milestone_parser_spec.rb
Normal file
31
spec/lib/banzai/reference_parser/milestone_parser_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::MilestoneParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:milestone) { create(:milestone, project: project) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
describe 'when the link has a data-milestone attribute' do
|
||||
context 'using an existing milestone ID' do
|
||||
it 'returns an Array of milestones' do
|
||||
link['data-milestone'] = milestone.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([milestone])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing milestone ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-milestone'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
spec/lib/banzai/reference_parser/snippet_parser_spec.rb
Normal file
31
spec/lib/banzai/reference_parser/snippet_parser_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::SnippetParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
let(:user) { create(:user) }
|
||||
let(:snippet) { create(:snippet, project: project) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
describe 'when the link has a data-snippet attribute' do
|
||||
context 'using an existing snippet ID' do
|
||||
it 'returns an Array of snippets' do
|
||||
link['data-snippet'] = snippet.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([snippet])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing snippet ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-snippet'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
189
spec/lib/banzai/reference_parser/user_parser_spec.rb
Normal file
189
spec/lib/banzai/reference_parser/user_parser_spec.rb
Normal file
|
@ -0,0 +1,189 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ReferenceParser::UserParser, lib: true do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:empty_project, :public, group: group, creator: user) }
|
||||
subject { described_class.new(project, user) }
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#referenced_by' do
|
||||
context 'when the link has a data-group attribute' do
|
||||
context 'using an existing group ID' do
|
||||
before do
|
||||
link['data-group'] = project.group.id.to_s
|
||||
end
|
||||
|
||||
it 'returns the users of the group' do
|
||||
create(:group_member, group: group, user: user)
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([user])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the group has no users' do
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing group ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-group'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link has a data-user attribute' do
|
||||
it 'returns an Array of users' do
|
||||
link['data-user'] = user.id.to_s
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link has a data-project attribute' do
|
||||
context 'using an existing project ID' do
|
||||
let(:contributor) { create(:user) }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
project.team << [contributor, :developer]
|
||||
end
|
||||
|
||||
it 'returns the members of a project' do
|
||||
link['data-project'] = project.id.to_s
|
||||
|
||||
# This uses an explicit sort to make sure this spec doesn't randomly
|
||||
# fail when objects are returned in a different order.
|
||||
refs = subject.referenced_by([link]).sort_by(&:id)
|
||||
|
||||
expect(refs).to eq([user, contributor])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a non-existing project ID' do
|
||||
it 'returns an empty Array' do
|
||||
link['data-project'] = ''
|
||||
|
||||
expect(subject.referenced_by([link])).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#nodes_visible_to_use?' do
|
||||
context 'when the link has a data-group attribute' do
|
||||
context 'using an existing group ID' do
|
||||
before do
|
||||
link['data-group'] = group.id.to_s
|
||||
end
|
||||
|
||||
it 'returns the nodes if the user can read the group' do
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_group, group).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array if the user can not read the group' do
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_group, group).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-group attribute' do
|
||||
context 'with a data-project attribute' do
|
||||
it 'returns the nodes if the attribute value equals the current project ID' do
|
||||
link['data-project'] = project.id.to_s
|
||||
|
||||
expect(Ability.abilities).not_to receive(:allowed?)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns the nodes if the user can read the project' do
|
||||
other_project = create(:empty_project, :public)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_project, other_project).
|
||||
and_return(true)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array if the user can not read the project' do
|
||||
other_project = create(:empty_project, :public)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
|
||||
expect(Ability.abilities).to receive(:allowed?).
|
||||
with(user, :read_project, other_project).
|
||||
and_return(false)
|
||||
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a data-project attribute' do
|
||||
it 'returns the nodes' do
|
||||
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#nodes_user_can_reference' do
|
||||
context 'when the link has a data-author attribute' do
|
||||
it 'returns the nodes when the user is a member of the project' do
|
||||
other_project = create(:project)
|
||||
other_project.team << [user, :developer]
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
link['data-author'] = user.id.to_s
|
||||
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the project could not be found' do
|
||||
link['data-project'] = ''
|
||||
link['data-author'] = user.id.to_s
|
||||
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user could not be found' do
|
||||
other_project = create(:project)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
link['data-author'] = ''
|
||||
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
|
||||
end
|
||||
|
||||
it 'returns an empty Array when the user is not a team member' do
|
||||
other_project = create(:project)
|
||||
|
||||
link['data-project'] = other_project.id.to_s
|
||||
link['data-author'] = user.id.to_s
|
||||
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link does not have a data-author attribute' do
|
||||
it 'returns the nodes' do
|
||||
expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
37
spec/lib/gitlab/lazy_spec.rb
Normal file
37
spec/lib/gitlab/lazy_spec.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Lazy, lib: true do
|
||||
let(:dummy) { double(:dummy) }
|
||||
|
||||
context 'when not calling any methods' do
|
||||
it 'does not call the supplied block' do
|
||||
expect(dummy).not_to receive(:foo)
|
||||
|
||||
described_class.new { dummy.foo }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when calling a method on the object' do
|
||||
it 'lazy loads the value returned by the block' do
|
||||
expect(dummy).to receive(:foo).and_return('foo')
|
||||
|
||||
lazy = described_class.new { dummy.foo }
|
||||
|
||||
expect(lazy.to_s).to eq('foo')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#respond_to?' do
|
||||
it 'returns true for a method defined on the wrapped object' do
|
||||
lazy = described_class.new { 'foo' }
|
||||
|
||||
expect(lazy).to respond_to(:downcase)
|
||||
end
|
||||
|
||||
it 'returns false for a method not defined on the wrapped object' do
|
||||
lazy = described_class.new { 'foo' }
|
||||
|
||||
expect(lazy).not_to respond_to(:quack)
|
||||
end
|
||||
end
|
||||
end
|
117
spec/models/ability_spec.rb
Normal file
117
spec/models/ability_spec.rb
Normal file
|
@ -0,0 +1,117 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Ability, lib: true do
|
||||
describe '.users_that_can_read_project' do
|
||||
context 'using a public project' do
|
||||
it 'returns all the users' do
|
||||
project = create(:project, :public)
|
||||
user = build(:user)
|
||||
|
||||
expect(described_class.users_that_can_read_project([user], project)).
|
||||
to eq([user])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using an internal project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
it 'returns users that are administrators' do
|
||||
user = build(:user, admin: true)
|
||||
|
||||
expect(described_class.users_that_can_read_project([user], project)).
|
||||
to eq([user])
|
||||
end
|
||||
|
||||
it 'returns internal users while skipping external users' do
|
||||
user1 = build(:user)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([user1])
|
||||
end
|
||||
|
||||
it 'returns external users if they are the project owner' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(project).to receive(:owner).twice.and_return(user1)
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([user1])
|
||||
end
|
||||
|
||||
it 'returns external users if they are project members' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(project.team).to receive(:members).twice.and_return([user1])
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([user1])
|
||||
end
|
||||
|
||||
it 'returns an empty Array if all users are external users without access' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
it 'returns users that are administrators' do
|
||||
user = build(:user, admin: true)
|
||||
|
||||
expect(described_class.users_that_can_read_project([user], project)).
|
||||
to eq([user])
|
||||
end
|
||||
|
||||
it 'returns external users if they are the project owner' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(project).to receive(:owner).twice.and_return(user1)
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([user1])
|
||||
end
|
||||
|
||||
it 'returns external users if they are project members' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(project.team).to receive(:members).twice.and_return([user1])
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([user1])
|
||||
end
|
||||
|
||||
it 'returns an empty Array if all users are internal users without access' do
|
||||
user1 = build(:user)
|
||||
user2 = build(:user)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([])
|
||||
end
|
||||
|
||||
it 'returns an empty Array if all users are external users without access' do
|
||||
user1 = build(:user, external: true)
|
||||
user2 = build(:user, external: true)
|
||||
users = [user1, user2]
|
||||
|
||||
expect(described_class.users_that_can_read_project(users, project)).
|
||||
to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -24,6 +24,16 @@ describe CommitRange, models: true do
|
|||
expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
it 'does not modify strings in-place' do
|
||||
input = "#{sha_from}...#{sha_to} "
|
||||
|
||||
described_class.new(input, project)
|
||||
|
||||
expect(input).to eq("#{sha_from}...#{sha_to} ")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_s' do
|
||||
it 'is correct for three-dot syntax' do
|
||||
expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
|
||||
|
@ -135,4 +145,27 @@ describe CommitRange, models: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_been_reverted?' do
|
||||
it 'returns true if the commit has been reverted' do
|
||||
issue = create(:issue)
|
||||
|
||||
create(:note_on_issue,
|
||||
noteable_id: issue.id,
|
||||
system: true,
|
||||
note: commit1.revert_description)
|
||||
|
||||
expect_any_instance_of(Commit).to receive(:reverts_commit?).
|
||||
with(commit1).
|
||||
and_return(true)
|
||||
|
||||
expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false a commit has not been reverted' do
|
||||
issue = create(:issue)
|
||||
|
||||
expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Commit, models: true do
|
||||
let(:project) { create(:project) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:commit) { project.commit }
|
||||
|
||||
describe 'modules' do
|
||||
|
@ -171,4 +171,40 @@ eos
|
|||
describe '#status' do
|
||||
# TODO: kamil
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
let(:user1) { build(:user) }
|
||||
let(:user2) { build(:user) }
|
||||
|
||||
let!(:note1) do
|
||||
create(:note_on_commit,
|
||||
commit_id: commit.id,
|
||||
project: project,
|
||||
note: 'foo')
|
||||
end
|
||||
|
||||
let!(:note2) do
|
||||
create(:note_on_commit,
|
||||
commit_id: commit.id,
|
||||
project: project,
|
||||
note: 'bar')
|
||||
end
|
||||
|
||||
before do
|
||||
allow(commit).to receive(:author).and_return(user1)
|
||||
allow(commit).to receive(:committer).and_return(user2)
|
||||
end
|
||||
|
||||
it 'includes the commit author' do
|
||||
expect(commit.participants).to include(commit.author)
|
||||
end
|
||||
|
||||
it 'includes the committer' do
|
||||
expect(commit.participants).to include(commit.committer)
|
||||
end
|
||||
|
||||
it 'includes the authors of the commit notes' do
|
||||
expect(commit.participants).to include(note1.author, note2.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
83
spec/models/concerns/participable_spec.rb
Normal file
83
spec/models/concerns/participable_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Participable, models: true do
|
||||
let(:model) do
|
||||
Class.new do
|
||||
include Participable
|
||||
end
|
||||
end
|
||||
|
||||
describe '.participant' do
|
||||
it 'adds the participant attributes to the existing list' do
|
||||
model.participant(:foo)
|
||||
model.participant(:bar)
|
||||
|
||||
expect(model.participant_attrs).to eq([:foo, :bar])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
it 'returns the list of participants' do
|
||||
model.participant(:foo)
|
||||
model.participant(:bar)
|
||||
|
||||
user1 = build(:user)
|
||||
user2 = build(:user)
|
||||
user3 = build(:user)
|
||||
project = build(:project, :public)
|
||||
instance = model.new
|
||||
|
||||
expect(instance).to receive(:foo).and_return(user2)
|
||||
expect(instance).to receive(:bar).and_return(user3)
|
||||
expect(instance).to receive(:project).twice.and_return(project)
|
||||
|
||||
participants = instance.participants(user1)
|
||||
|
||||
expect(participants).to include(user2)
|
||||
expect(participants).to include(user3)
|
||||
end
|
||||
|
||||
it 'supports attributes returning another Participable' do
|
||||
other_model = Class.new { include Participable }
|
||||
|
||||
other_model.participant(:bar)
|
||||
model.participant(:foo)
|
||||
|
||||
instance = model.new
|
||||
other = other_model.new
|
||||
user1 = build(:user)
|
||||
user2 = build(:user)
|
||||
project = build(:project, :public)
|
||||
|
||||
expect(instance).to receive(:foo).and_return(other)
|
||||
expect(other).to receive(:bar).and_return(user2)
|
||||
expect(instance).to receive(:project).twice.and_return(project)
|
||||
|
||||
expect(instance.participants(user1)).to eq([user2])
|
||||
end
|
||||
|
||||
context 'when using a Proc as an attribute' do
|
||||
it 'calls the supplied Proc' do
|
||||
user1 = build(:user)
|
||||
project = build(:project, :public)
|
||||
|
||||
user_arg = nil
|
||||
ext_arg = nil
|
||||
|
||||
model.participant -> (user, ext) do
|
||||
user_arg = user
|
||||
ext_arg = ext
|
||||
end
|
||||
|
||||
instance = model.new
|
||||
|
||||
expect(instance).to receive(:project).twice.and_return(project)
|
||||
|
||||
instance.participants(user1)
|
||||
|
||||
expect(user_arg).to eq(user1)
|
||||
expect(ext_arg).to be_an_instance_of(Gitlab::ReferenceExtractor)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -231,4 +231,42 @@ describe Issue, models: true do
|
|||
expect(issue.to_branch_name).to match /confidential-issue\z/
|
||||
end
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
context 'using a public project' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
let!(:note1) do
|
||||
create(:note_on_issue, noteable: issue, project: project, note: 'a')
|
||||
end
|
||||
|
||||
let!(:note2) do
|
||||
create(:note_on_issue, noteable: issue, project: project, note: 'b')
|
||||
end
|
||||
|
||||
it 'includes the issue author' do
|
||||
expect(issue.participants).to include(issue.author)
|
||||
end
|
||||
|
||||
it 'includes the authors of the notes' do
|
||||
expect(issue.participants).to include(note1.author, note2.author)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project' do
|
||||
it 'does not include mentioned users that do not have access to the project' do
|
||||
project = create(:project)
|
||||
user = create(:user)
|
||||
issue = create(:issue, project: project)
|
||||
|
||||
create(:note_on_issue,
|
||||
noteable: issue,
|
||||
project: project,
|
||||
note: user.to_reference)
|
||||
|
||||
expect(issue.participants).not_to include(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -414,4 +414,28 @@ describe MergeRequest, models: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
let(:mr) do
|
||||
create(:merge_request, source_project: project, target_project: project)
|
||||
end
|
||||
|
||||
let!(:note1) do
|
||||
create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
|
||||
end
|
||||
|
||||
let!(:note2) do
|
||||
create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
|
||||
end
|
||||
|
||||
it 'includes the merge request author' do
|
||||
expect(mr.participants).to include(mr.author)
|
||||
end
|
||||
|
||||
it 'includes the authors of the notes' do
|
||||
expect(mr.participants).to include(note1.author, note2.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,8 +121,19 @@ describe Note, models: true do
|
|||
let!(:note2) { create(:note_on_issue) }
|
||||
|
||||
it "reads the rendered note body from the cache" do
|
||||
expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project)
|
||||
expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project)
|
||||
expect(Banzai::Renderer).to receive(:render).
|
||||
with(note1.note,
|
||||
pipeline: :note,
|
||||
cache_key: [note1, "note"],
|
||||
project: note1.project,
|
||||
author: note1.author)
|
||||
|
||||
expect(Banzai::Renderer).to receive(:render).
|
||||
with(note2.note,
|
||||
pipeline: :note,
|
||||
cache_key: [note2, "note"],
|
||||
project: note2.project,
|
||||
author: note2.author)
|
||||
|
||||
note1.all_references
|
||||
note2.all_references
|
||||
|
@ -248,4 +259,14 @@ describe Note, models: true do
|
|||
expect { note.valid? }.to change(note, :line_code).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
it 'includes the note author' do
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, project: project)
|
||||
note = create(:note_on_issue, noteable: issue, project: project)
|
||||
|
||||
expect(note.participants).to include(note.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,4 +87,31 @@ describe Snippet, models: true do
|
|||
expect(described_class.search_code('FOO')).to eq([snippet])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#participants' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:snippet) { create(:snippet, content: 'foo', project: project) }
|
||||
|
||||
let!(:note1) do
|
||||
create(:note_on_project_snippet,
|
||||
noteable: snippet,
|
||||
project: project,
|
||||
note: 'a')
|
||||
end
|
||||
|
||||
let!(:note2) do
|
||||
create(:note_on_project_snippet,
|
||||
noteable: snippet,
|
||||
project: project,
|
||||
note: 'b')
|
||||
end
|
||||
|
||||
it 'includes the snippet author' do
|
||||
expect(snippet.participants).to include(snippet.author)
|
||||
end
|
||||
|
||||
it 'includes the note authors' do
|
||||
expect(snippet.participants).to include(note1.author, note2.author)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,8 +40,7 @@ module FilterSpecHelper
|
|||
|
||||
filters = [
|
||||
Banzai::Filter::AutolinkFilter,
|
||||
described_class,
|
||||
Banzai::Filter::ReferenceGathererFilter
|
||||
described_class
|
||||
]
|
||||
|
||||
HTML::Pipeline.new(filters, context)
|
||||
|
|
5
spec/support/reference_parser_helpers.rb
Normal file
5
spec/support/reference_parser_helpers.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module ReferenceParserHelpers
|
||||
def empty_html_link
|
||||
Nokogiri::HTML.fragment('<a></a>').children[0]
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue