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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-22 15:09:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-22 15:09:49 +0300
commit4b074c5f634f8e1e550107f9e8237f07878ca0e8 (patch)
tree00afed4a6853548ec97203f3f807d954180b547d /lib
parentb81fd57f3d62db4455108c8de4b8d7b8d403de35 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib')
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb51
-rw-r--r--lib/banzai/filter/references/alert_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/commit_range_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb9
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb9
-rw-r--r--lib/banzai/filter/references/external_issue_reference_filter.rb37
-rw-r--r--lib/banzai/filter/references/feature_flag_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/iteration_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/label_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/merge_request_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/milestone_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/project_reference_filter.rb36
-rw-r--r--lib/banzai/filter/references/reference_filter.rb125
-rw-r--r--lib/banzai/filter/references/snippet_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb36
-rw-r--r--lib/banzai/filter/references/vulnerability_reference_filter.rb11
-rw-r--r--lib/gitlab/graphql/deprecation.rb2
-rw-r--r--lib/gitlab/graphql/docs/helper.rb381
-rw-r--r--lib/gitlab/graphql/docs/renderer.rb11
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml142
21 files changed, 573 insertions, 328 deletions
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
index 7109373dbce..2763e084de9 100644
--- a/lib/banzai/filter/references/abstract_reference_filter.rb
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -16,22 +16,9 @@ module Banzai
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
- def self.object_class
- # Implement in child class
- # Example: MergeRequest
- end
-
- def self.object_name
- @object_name ||= object_class.name.underscore
- end
-
- def self.object_sym
- @object_sym ||= object_name.to_sym
- end
-
# Public: Find references in text (like `!123` for merge requests)
#
- # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # references_in(text) do |match, id, project_ref, matches|
# object = find_object(project_ref, id)
# "<a href=...>#{object.to_reference}</a>"
# end
@@ -42,7 +29,7 @@ module Banzai
# of the external project reference, and all of the matchdata.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text, pattern = object_class.reference_pattern)
+ def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
if ident = identifier($~)
yield match, ident, $~[:project], $~[:namespace], $~
@@ -52,17 +39,13 @@ module Banzai
end
end
- def self.identifier(match_data)
+ def identifier(match_data)
symbol = symbol_from_match(match_data)
parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
end
- def identifier(match_data)
- self.class.identifier(match_data)
- end
-
- def self.symbol_from_match(match)
+ def symbol_from_match(match)
key = object_sym
match[key] if match.names.include?(key.to_s)
end
@@ -72,7 +55,7 @@ module Banzai
#
# This method has the contract that if a string `ref` refers to a
# record `record`, then `parse_symbol(ref) == record_identifier(record)`.
- def self.parse_symbol(symbol, match_data)
+ def parse_symbol(symbol, match_data)
symbol.to_i
end
@@ -84,21 +67,10 @@ module Banzai
record.id
end
- def object_class
- self.class.object_class
- end
-
- def object_sym
- self.class.object_sym
- end
-
- def references_in(*args, &block)
- self.class.references_in(*args, &block)
- end
-
# Implement in child class
# Example: project.merge_requests.find
def find_object(parent_object, id)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end
# Override if the link reference pattern produces a different ID (global
@@ -110,6 +82,7 @@ module Banzai
# Implement in child class
# Example: project_merge_request_url
def url_for_object(object, parent_object)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end
def find_object_cached(parent_object, id)
@@ -139,7 +112,7 @@ module Banzai
def call
return doc unless project || group || user
- ref_pattern = object_class.reference_pattern
+ ref_pattern = object_reference_pattern
link_pattern = object_class.link_reference_pattern
# Compile often used regexps only once outside of the loop
@@ -425,14 +398,6 @@ module Banzai
group_ref
end
- def unescape_html_entities(text)
- CGI.unescapeHTML(text.to_s)
- end
-
- def escape_html_entities(text)
- CGI.escapeHTML(text.to_s)
- end
-
def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text)
diff --git a/lib/banzai/filter/references/alert_reference_filter.rb b/lib/banzai/filter/references/alert_reference_filter.rb
index 90fef536605..512d4028520 100644
--- a/lib/banzai/filter/references/alert_reference_filter.rb
+++ b/lib/banzai/filter/references/alert_reference_filter.rb
@@ -5,12 +5,9 @@ module Banzai
module References
class AlertReferenceFilter < IssuableReferenceFilter
self.reference_type = :alert
+ self.object_class = AlertManagement::Alert
- def self.object_class
- AlertManagement::Alert
- end
-
- def self.object_sym
+ def object_sym
:alert
end
diff --git a/lib/banzai/filter/references/commit_range_reference_filter.rb b/lib/banzai/filter/references/commit_range_reference_filter.rb
index ad79f8a173c..df7f42eaa70 100644
--- a/lib/banzai/filter/references/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_range_reference_filter.rb
@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range
+ self.object_class = CommitRange
- def self.object_class
- CommitRange
- end
-
- def self.references_in(text, pattern = CommitRange.reference_pattern)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
end
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
index 457921bd07d..1baafeccbd9 100644
--- a/lib/banzai/filter/references/commit_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit
+ self.object_class = Commit
- def self.object_class
- Commit
- end
-
- def self.references_in(text, pattern = Commit.reference_pattern)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:commit], $~[:project], $~[:namespace], $~
end
@@ -39,7 +36,7 @@ module Banzai
end
# The default behaviour is `#to_i` - we just pass the hash through.
- def self.parse_symbol(sha_hash, _match)
+ def parse_symbol(sha_hash, _match)
sha_hash
end
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
index 61234e61c15..de8d58de72f 100644
--- a/lib/banzai/filter/references/design_reference_filter.rb
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -33,6 +33,7 @@ module Banzai
end
self.reference_type = :design
+ self.object_class = ::DesignManagement::Design
def find_object(project, identifier)
records_per_parent[project][identifier]
@@ -76,15 +77,11 @@ module Banzai
super.merge(issue: design.issue_id)
end
- def self.object_class
- ::DesignManagement::Design
- end
-
- def self.object_sym
+ def object_sym
:design
end
- def self.parse_symbol(raw, match_data)
+ def parse_symbol(raw, match_data)
filename = match_data[:url_filename]
iid = match_data[:issue].to_i
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
diff --git a/lib/banzai/filter/references/external_issue_reference_filter.rb b/lib/banzai/filter/references/external_issue_reference_filter.rb
index 247e20967df..1061a9917dd 100644
--- a/lib/banzai/filter/references/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/references/external_issue_reference_filter.rb
@@ -10,10 +10,11 @@ module Banzai
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
+ self.object_class = ExternalIssue
# Public: Find `JIRA-123` issue references in text
#
- # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
+ # references_in(text, pattern) do |match, issue|
# "<a href=...>##{issue}</a>"
# end
#
@@ -22,7 +23,7 @@ module Banzai
# Yields the String match and the String issue reference.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text, pattern)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:issue]
end
@@ -32,27 +33,7 @@ module Banzai
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
- ref_pattern = issue_reference_pattern
- ref_start_pattern = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- issue_link_filter(content)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_start_pattern
- replace_link_node_with_href(node, index, link) do
- issue_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
+ super
end
private
@@ -65,8 +46,8 @@ module Banzai
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_content: nil)
- self.class.references_in(text, issue_reference_pattern) do |match, id|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text) do |match, id|
url = url_for_issue(id)
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
@@ -97,14 +78,10 @@ module Banzai
external_issues_cached(:default_issues_tracker?)
end
- def issue_reference_pattern
+ def object_reference_pattern
external_issues_cached(:external_issue_reference_pattern)
end
- def project
- context[:project]
- end
-
def issue_title
"Issue in #{project.external_issue_tracker.title}"
end
diff --git a/lib/banzai/filter/references/feature_flag_reference_filter.rb b/lib/banzai/filter/references/feature_flag_reference_filter.rb
index be9ded1ff43..0fb2b1b3b24 100644
--- a/lib/banzai/filter/references/feature_flag_reference_filter.rb
+++ b/lib/banzai/filter/references/feature_flag_reference_filter.rb
@@ -5,12 +5,9 @@ module Banzai
module References
class FeatureFlagReferenceFilter < IssuableReferenceFilter
self.reference_type = :feature_flag
+ self.object_class = Operations::FeatureFlag
- def self.object_class
- Operations::FeatureFlag
- end
-
- def self.object_sym
+ def object_sym
:feature_flag
end
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
index eacf261b15f..1053501de7b 100644
--- a/lib/banzai/filter/references/issue_reference_filter.rb
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -13,10 +13,7 @@ module Banzai
# to reference issues from other GitLab projects.
class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue
-
- def self.object_class
- Issue
- end
+ self.object_class = Issue
def url_for_object(issue, project)
return issue_path(issue, project) if only_path?
diff --git a/lib/banzai/filter/references/iteration_reference_filter.rb b/lib/banzai/filter/references/iteration_reference_filter.rb
index cf3d446147f..4d260983d98 100644
--- a/lib/banzai/filter/references/iteration_reference_filter.rb
+++ b/lib/banzai/filter/references/iteration_reference_filter.rb
@@ -6,10 +6,7 @@ module Banzai
# The actual filter is implemented in the EE mixin
class IterationReferenceFilter < AbstractReferenceFilter
self.reference_type = :iteration
-
- def self.object_class
- Iteration
- end
+ self.object_class = Iteration
end
end
end
diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb
index a6a5eec5d9a..f9668d22d40 100644
--- a/lib/banzai/filter/references/label_reference_filter.rb
+++ b/lib/banzai/filter/references/label_reference_filter.rb
@@ -6,10 +6,7 @@ module Banzai
# HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label
-
- def self.object_class
- Label
- end
+ self.object_class = Label
def find_object(parent_object, id)
find_labels(parent_object).find(id)
diff --git a/lib/banzai/filter/references/merge_request_reference_filter.rb b/lib/banzai/filter/references/merge_request_reference_filter.rb
index 872c33f6873..6c5ad83d9ae 100644
--- a/lib/banzai/filter/references/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/references/merge_request_reference_filter.rb
@@ -9,10 +9,7 @@ module Banzai
# This filter supports cross-project references.
class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request
-
- def self.object_class
- MergeRequest
- end
+ self.object_class = MergeRequest
def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers
diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb
index 49110194ddc..c7e4b8b35a2 100644
--- a/lib/banzai/filter/references/milestone_reference_filter.rb
+++ b/lib/banzai/filter/references/milestone_reference_filter.rb
@@ -8,10 +8,7 @@ module Banzai
include Gitlab::Utils::StrongMemoize
self.reference_type = :milestone
-
- def self.object_class
- Milestone
- end
+ self.object_class = Milestone
# Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate
diff --git a/lib/banzai/filter/references/project_reference_filter.rb b/lib/banzai/filter/references/project_reference_filter.rb
index 522c6e0f5f3..678d2aa3468 100644
--- a/lib/banzai/filter/references/project_reference_filter.rb
+++ b/lib/banzai/filter/references/project_reference_filter.rb
@@ -6,10 +6,11 @@ module Banzai
# HTML filter that replaces project references with links.
class ProjectReferenceFilter < ReferenceFilter
self.reference_type = :project
+ self.object_class = Project
# Public: Find `namespace/project>` project references in text
#
- # ProjectReferenceFilter.references_in(text) do |match, project|
+ # references_in(text) do |match, project|
# "<a href=...>#{project}></a>"
# end
#
@@ -18,33 +19,16 @@ module Banzai
# Yields the String match, and the String project name.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Project.markdown_reference_pattern) do |match|
+ def references_in(text, pattern = object_reference_pattern)
+ text.gsub(pattern) do |match|
yield match, "#{$~[:namespace]}/#{$~[:project]}"
end
end
- def call
- ref_pattern = Project.markdown_reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- project_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- project_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
+ private
- doc
+ def object_reference_pattern
+ @object_reference_pattern ||= Project.markdown_reference_pattern
end
# Replace `namespace/project>` project references in text with links to the referenced
@@ -55,8 +39,8 @@ module Banzai
#
# Returns a String with `namespace/project>` references replaced with links. All links
# have `gfm` and `gfm-project` class names attached for styling.
- def project_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, project_path|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text) do |match, project_path|
cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
if project = projects_hash[project_path.downcase]
link_to_project(project, link_content: link_content) || match
@@ -92,8 +76,6 @@ module Banzai
refs.to_a
end
- private
-
def urls
Gitlab::Routing.url_helpers
end
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
index dd15c43f5d8..a83cb12afd3 100644
--- a/lib/banzai/filter/references/reference_filter.rb
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -16,8 +16,14 @@ module Banzai
include OutputSafety
class << self
+ # Implement in child class
+ # Example: self.reference_type = :merge_request
attr_accessor :reference_type
+ # Implement in child class
+ # Example: self.object_class = MergeRequest
+ attr_accessor :object_class
+
def call(doc, context = nil, result = nil)
new(doc, context, result).call_and_update_nodes
end
@@ -34,6 +40,65 @@ module Banzai
with_update_nodes { call }
end
+ def call
+ ref_pattern_start = /\A#{object_reference_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, object_reference_pattern) do |content|
+ object_link_filter(content, object_reference_pattern)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, object_reference_pattern, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def references_in(text, pattern = object_reference_pattern)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+ end
+
+ # Iterates over all <a> and text() nodes in a document.
+ #
+ # Nodes are skipped whenever their ancestor is one of the nodes returned
+ # by `ignore_ancestor_query`. Link tags are not processed if they have a
+ # "gfm" class or the "href" attribute is empty.
+ def each_node
+ return to_enum(__method__) unless block_given?
+
+ doc.xpath(query).each do |node|
+ yield node
+ end
+ end
+
+ # Returns an Array containing all HTML nodes.
+ def nodes
+ @nodes ||= each_node.to_a
+ end
+
+ private
+
# Returns a data attribute String to attach to a reference link
#
# attributes - Hash, where the key becomes the data attribute name and the
@@ -69,6 +134,13 @@ module Banzai
end
end
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project unless skip_project_check?
+ end
+
def project
context[:project]
end
@@ -93,31 +165,6 @@ module Banzai
"#{gfm_klass} has-tooltip"
end
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project unless skip_project_check?
- end
-
- # Iterates over all <a> and text() nodes in a document.
- #
- # Nodes are skipped whenever their ancestor is one of the nodes returned
- # by `ignore_ancestor_query`. Link tags are not processed if they have a
- # "gfm" class or the "href" attribute is empty.
- def each_node
- return to_enum(__method__) unless block_given?
-
- doc.xpath(query).each do |node|
- yield node
- end
- end
-
- # Returns an Array containing all HTML nodes.
- def nodes
- @nodes ||= each_node.to_a
- end
-
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = unescape_link(node.attr('href').to_s)
@@ -132,6 +179,14 @@ module Banzai
CGI.unescape(href)
end
+ def unescape_html_entities(text)
+ CGI.unescapeHTML(text.to_s)
+ end
+
+ def escape_html_entities(text)
+ CGI.escapeHTML(text.to_s)
+ end
+
def replace_text_when_pattern_matches(node, index, pattern)
return unless node.text =~ pattern
@@ -161,7 +216,25 @@ module Banzai
node.is_a?(Nokogiri::XML::Element)
end
- private
+ def object_class
+ self.class.object_class
+ end
+
+ def object_reference_pattern
+ @object_reference_pattern ||= object_class.reference_pattern
+ end
+
+ def object_name
+ @object_name ||= object_class.name.underscore
+ end
+
+ def object_sym
+ @object_sym ||= object_name.to_sym
+ end
+
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+ end
def query
@query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
diff --git a/lib/banzai/filter/references/snippet_reference_filter.rb b/lib/banzai/filter/references/snippet_reference_filter.rb
index bf7e0f78609..a2971e28de6 100644
--- a/lib/banzai/filter/references/snippet_reference_filter.rb
+++ b/lib/banzai/filter/references/snippet_reference_filter.rb
@@ -9,10 +9,7 @@ module Banzai
# This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet
-
- def self.object_class
- Snippet
- end
+ self.object_class = Snippet
def find_object(project, id)
return unless project.is_a?(Project)
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index 04665973f51..1709b607c2e 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -8,10 +8,11 @@ module Banzai
# A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter
self.reference_type = :user
+ self.object_class = User
# Public: Find `@user` user references in text
#
- # UserReferenceFilter.references_in(text) do |match, username|
+ # references_in(text) do |match, username|
# "<a href=...>@#{user}</a>"
# end
#
@@ -20,8 +21,8 @@ module Banzai
# Yields the String match, and the String user name.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(User.reference_pattern) do |match|
+ def references_in(text, pattern = object_reference_pattern)
+ text.gsub(pattern) do |match|
yield match, $~[:user]
end
end
@@ -29,28 +30,11 @@ module Banzai
def call
return doc if project.nil? && group.nil? && !skip_project_check?
- ref_pattern = User.reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- user_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- user_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
+ super
end
+ private
+
# Replace `@user` user references in text with links to the referenced
# user's profile page.
#
@@ -59,8 +43,8 @@ module Banzai
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, username|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text, pattern) do |match, username|
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
else
@@ -100,8 +84,6 @@ module Banzai
refs.to_a
end
- private
-
def urls
Gitlab::Routing.url_helpers
end
diff --git a/lib/banzai/filter/references/vulnerability_reference_filter.rb b/lib/banzai/filter/references/vulnerability_reference_filter.rb
index e5f2408eda4..ce484667fc0 100644
--- a/lib/banzai/filter/references/vulnerability_reference_filter.rb
+++ b/lib/banzai/filter/references/vulnerability_reference_filter.rb
@@ -6,16 +6,7 @@ module Banzai
# The actual filter is implemented in the EE mixin
class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability
-
- def self.object_class
- Vulnerability
- end
-
- private
-
- def project
- context[:project]
- end
+ self.object_class = Vulnerability
end
end
end
diff --git a/lib/gitlab/graphql/deprecation.rb b/lib/gitlab/graphql/deprecation.rb
index e0176e2d6e0..8b73eeb4e52 100644
--- a/lib/gitlab/graphql/deprecation.rb
+++ b/lib/gitlab/graphql/deprecation.rb
@@ -41,7 +41,7 @@ module Gitlab
parts = [
"#{deprecated_in(format: :markdown)}.",
reason_text,
- replacement.then { |r| "Use: `#{r}`." if r }
+ replacement.then { |r| "Use: [`#{r}`](##{r.downcase.tr('.', '')})." if r }
].compact
case context
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index f4173e26224..ce5fb575b54 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -5,11 +5,52 @@ return if Rails.env.production?
module Gitlab
module Graphql
module Docs
+ # We assume a few things about the schema. We use the graphql-ruby gem, which enforces:
+ # - All mutations have a single input field named 'input'
+ # - All mutations have a payload type, named after themselves
+ # - All mutations have an input type, named after themselves
+ # If these things change, then some of this code will break. Such places
+ # are guarded with an assertion that our assumptions are not violated.
+ ViolatedAssumption = Class.new(StandardError)
+
+ SUGGESTED_ACTION = <<~MSG
+ We expect it to be impossible to violate our assumptions about
+ how mutation arguments work.
+
+ If that is not the case, then something has probably changed in the
+ way we generate our schema, perhaps in the library we use: graphql-ruby
+
+ Please ask for help in the #f_graphql or #backend channels.
+ MSG
+
+ CONNECTION_ARGS = %w[after before first last].to_set
+
+ FIELD_HEADER = <<~MD
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ MD
+
+ ARG_HEADER = <<~MD
+ # Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ MD
+
+ CONNECTION_NOTE = <<~MD
+ This field returns a [connection](#connections). It accepts the
+ four standard [pagination arguments](#connection-pagination-arguments):
+ `before: String`, `after: String`, `first: Int`, `last: Int`.
+ MD
+
# Helper with functions to be used by HAML templates
# This includes graphql-docs gem helpers class.
# You can check the included module on: https://github.com/gjtorikian/graphql-docs/blob/v1.6.0/lib/graphql-docs/helpers.rb
module Helper
include GraphQLDocs::Helpers
+ include Gitlab::Utils::StrongMemoize
def auto_generated_comment
<<-MD.strip_heredoc
@@ -30,44 +71,52 @@ module Gitlab
# Template methods:
# Methods that return chunks of Markdown for insertion into the document
- def render_name_and_description(object, owner: nil, level: 3)
- content = []
+ def render_full_field(field, heading_level: 3, owner: nil)
+ conn = connection?(field)
+ args = field[:arguments].reject { |arg| conn && CONNECTION_ARGS.include?(arg[:name]) }
+ arg_owner = [owner, field[:name]]
+
+ chunks = [
+ render_name_and_description(field, level: heading_level, owner: owner),
+ render_return_type(field),
+ render_input_type(field),
+ render_connection_note(field),
+ render_argument_table(heading_level, args, arg_owner),
+ render_return_fields(field, owner: owner)
+ ]
+
+ join(:block, chunks)
+ end
- content << "#{'#' * level} `#{object[:name]}`"
+ def render_argument_table(level, args, owner)
+ arg_header = ('#' * level) + ARG_HEADER
+ render_field_table(arg_header, args, owner)
+ end
- if object[:description].present?
- desc = object[:description].strip
- desc += '.' unless desc.ends_with?('.')
- end
+ def render_name_and_description(object, owner: nil, level: 3)
+ content = []
- if object[:is_deprecated]
- owner = Array.wrap(owner)
- deprecation = schema_deprecation(owner, object[:name])
- content << (deprecation&.original_description || desc)
- content << render_deprecation(object, owner, :block)
- else
- content << desc
- end
+ heading = '#' * level
+ name = [owner, object[:name]].compact.join('.')
- content.compact.join("\n\n")
- end
+ content << "#{heading} `#{name}`"
+ content << render_description(object, owner, :block)
- def render_return_type(query)
- "Returns #{render_field_type(query[:type])}.\n"
+ join(:block, content)
end
- def sorted_by_name(objects)
- return [] unless objects.present?
+ def render_object_fields(fields, owner:, level_bump: 0)
+ return if fields.blank?
- objects.sort_by { |o| o[:name] }
- end
+ (with_args, no_args) = fields.partition { |f| args?(f) }
+ type_name = owner[:name] if owner
+ header_prefix = '#' * level_bump
+ sections = [
+ render_simple_fields(no_args, type_name, header_prefix),
+ render_fields_with_arguments(with_args, type_name, header_prefix)
+ ]
- def render_field(field, owner)
- render_row(
- render_name(field, owner),
- render_field_type(field[:type]),
- render_description(field, owner, :inline)
- )
+ join(:block, sections)
end
def render_enum_value(enum, value)
@@ -82,104 +131,278 @@ module Gitlab
# Methods that return parts of the schema, or related information:
- # We are ignoring connections and built in types for now,
- # they should be added when queries are generated.
- def objects
- object_types = graphql_object_types.select do |object_type|
- !object_type[:name]["__"]
- end
+ def connection_object_types
+ objects.select { |t| t[:is_edge] || t[:is_connection] }
+ end
+
+ def object_types
+ objects.reject { |t| t[:is_edge] || t[:is_connection] || t[:is_payload] }
+ end
+
+ def interfaces
+ graphql_interface_types.map { |t| t.merge(fields: t[:fields] + t[:connections]) }
+ end
+
+ def fields_of(type_name)
+ graphql_operation_types
+ .find { |type| type[:name] == type_name }
+ .values_at(:fields, :connections)
+ .flatten
+ .then { |fields| sorted_by_name(fields) }
+ end
+
+ # Place the arguments of the input types on the mutation itself.
+ # see: `#input_types` - this method must not call `#input_types` to avoid mutual recursion
+ def mutations
+ @mutations ||= sorted_by_name(graphql_mutation_types).map do |t|
+ inputs = t[:input_fields]
+ input = inputs.first
+ name = t[:name]
+
+ assert!(inputs.one?, "Expected exactly 1 input field named #{name}. Found #{inputs.count} instead.")
+ assert!(input[:name] == 'input', "Expected the input of #{name} to be named 'input'")
- object_types.each do |type|
- type[:fields] += type[:connections]
+ input_type_name = input[:type][:name]
+ input_type = graphql_input_object_types.find { |t| t[:name] == input_type_name }
+ assert!(input_type.present?, "Cannot find #{input_type_name} for #{name}.input")
+
+ arguments = input_type[:input_fields]
+ seen_type!(input_type_name)
+ t.merge(arguments: arguments)
end
end
- def queries
- graphql_operation_types.find { |type| type[:name] == 'Query' }.to_h.values_at(:fields, :connections).flatten
+ # We assume that the mutations have been processed first, marking their
+ # inputs as `seen_type?`
+ def input_types
+ mutations # ensure that mutations have seen their inputs first
+ graphql_input_object_types.reject { |t| seen_type?(t[:name]) }
end
- # We ignore the built-in enum types.
+ # We ignore the built-in enum types, and sort values by name
def enums
- graphql_enum_types.select do |enum_type|
- !enum_type[:name].in?(%w[__DirectiveLocation __TypeKind])
- end
+ graphql_enum_types
+ .reject { |type| type[:values].empty? }
+ .reject { |enum_type| enum_type[:name].start_with?('__') }
+ .map { |type| type.merge(values: sorted_by_name(type[:values])) }
end
private # DO NOT CALL THESE METHODS IN TEMPLATES
# Template methods
+ def render_return_type(query)
+ return unless query[:type] # for example, mutations
+
+ "Returns #{render_field_type(query[:type])}."
+ end
+
+ def render_simple_fields(fields, type_name, header_prefix)
+ render_field_table(header_prefix + FIELD_HEADER, fields, type_name)
+ end
+
+ def render_fields_with_arguments(fields, type_name, header_prefix)
+ return if fields.empty?
+
+ level = 5 + header_prefix.length
+ sections = sorted_by_name(fields).map do |f|
+ render_full_field(f, heading_level: level, owner: type_name)
+ end
+
+ <<~MD.chomp
+ #{header_prefix}#### Fields with arguments
+
+ #{join(:block, sections)}
+ MD
+ end
+
+ def render_field_table(header, fields, owner)
+ return if fields.empty?
+
+ fields = sorted_by_name(fields)
+ header + join(:table, fields.map { |f| render_field(f, owner) })
+ end
+
+ def render_field(field, owner)
+ render_row(
+ render_name(field, owner),
+ render_field_type(field[:type]),
+ render_description(field, owner, :inline)
+ )
+ end
+
+ def render_return_fields(mutation, owner:)
+ fields = mutation[:return_fields]
+ return if fields.blank?
+
+ name = owner.to_s + mutation[:name]
+ render_object_fields(fields, owner: { name: name })
+ end
+
+ def render_connection_note(field)
+ return unless connection?(field)
+
+ CONNECTION_NOTE.chomp
+ end
+
def render_row(*values)
"| #{values.map { |val| val.to_s.squish }.join(' | ')} |"
end
def render_name(object, owner = nil)
rendered_name = "`#{object[:name]}`"
- rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
- rendered_name
+ rendered_name += ' **{warning-solid}**' if deprecated?(object, owner)
+
+ return rendered_name unless owner
+
+ owner = Array.wrap(owner).join('')
+ id = (owner + object[:name]).downcase
+
+ %(<a id="#{id}"></a>) + rendered_name
end
# Returns the object description. If the object has been deprecated,
# the deprecation reason will be returned in place of the description.
def render_description(object, owner = nil, context = :block)
- owner = Array.wrap(owner)
- return render_deprecation(object, owner, context) if object[:is_deprecated]
- return if object[:description].blank?
+ if deprecated?(object, owner)
+ render_deprecation(object, owner, context)
+ else
+ render_description_of(object)
+ end
+ end
+
+ def deprecated?(object, owner)
+ return true if object[:is_deprecated] # only populated for fields, not arguments!
+
+ key = [*Array.wrap(owner), object[:name]].join('.')
+ deprecations.key?(key)
+ end
+
+ def render_description_of(object)
+ desc = if object[:is_edge]
+ base = object[:name].chomp('Edge')
+ "The edge type for [`#{base}`](##{base.downcase})."
+ elsif object[:is_connection]
+ base = object[:name].chomp('Connection')
+ "The connection type for [`#{base}`](##{base.downcase})."
+ else
+ object[:description]&.strip
+ end
+
+ return if desc.blank?
- desc = object[:description].strip
desc += '.' unless desc.ends_with?('.')
desc
end
def render_deprecation(object, owner, context)
+ buff = []
deprecation = schema_deprecation(owner, object[:name])
- return deprecation.markdown(context: context) if deprecation
- reason = object[:deprecation_reason] || 'Use of this is deprecated.'
- "**Deprecated:** #{reason}"
+ buff << (deprecation&.original_description || render_description_of(object)) if context == :block
+ buff << if deprecation
+ deprecation.markdown(context: context)
+ else
+ "**Deprecated:** #{object[:deprecation_reason]}"
+ end
+
+ join(context, buff)
end
def render_field_type(type)
"[`#{type[:info]}`](##{type[:name].downcase})"
end
+ def join(context, chunks)
+ chunks.compact!
+ return if chunks.blank?
+
+ case context
+ when :block
+ chunks.join("\n\n")
+ when :inline
+ chunks.join(" ").squish.presence
+ when :table
+ chunks.join("\n")
+ end
+ end
+
# Queries
+ def sorted_by_name(objects)
+ return [] unless objects.present?
+
+ objects.sort_by { |o| o[:name] }
+ end
+
+ def connection?(field)
+ type_name = field.dig(:type, :name)
+ type_name.present? && type_name.ends_with?('Connection')
+ end
+
+ # We are ignoring connections and built in types for now,
+ # they should be added when queries are generated.
+ def objects
+ strong_memoize(:objects) do
+ mutations = schema.mutation&.fields&.keys&.to_set || []
+
+ graphql_object_types
+ .reject { |object_type| object_type[:name]["__"] } # We ignore introspection types.
+ .map do |type|
+ name = type[:name]
+ type.merge(
+ is_edge: name.ends_with?('Edge'),
+ is_connection: name.ends_with?('Connection'),
+ is_payload: name.ends_with?('Payload') && mutations.include?(name.chomp('Payload').camelcase(:lower)),
+ fields: type[:fields] + type[:connections]
+ )
+ end
+ end
+ end
+
+ def args?(field)
+ args = field[:arguments]
+ return false if args.blank?
+ return true unless connection?(field)
+
+ args.any? { |arg| CONNECTION_ARGS.exclude?(arg[:name]) }
+ end
+
# returns the deprecation information for a field or argument
# See: Gitlab::Graphql::Deprecation
def schema_deprecation(type_name, field_name)
- schema_member(type_name, field_name)&.deprecation
- end
-
- # Return a part of the schema.
- #
- # This queries the Schema by owner and name to find:
- #
- # - fields (e.g. `schema_member('Query', 'currentUser')`)
- # - arguments (e.g. `schema_member(['Query', 'project], 'fullPath')`)
- def schema_member(type_name, field_name)
- type_name = Array.wrap(type_name)
- if type_name.size == 2
- arg_name = field_name
- type_name, field_name = type_name
- else
- type_name = type_name.first
- arg_name = nil
- end
+ key = [*Array.wrap(type_name), field_name].join('.')
+ deprecations[key]
+ end
- return if type_name.nil? || field_name.nil?
+ def render_input_type(query)
+ input_field = query[:input_fields]&.first
+ return unless input_field
+
+ "Input type: `#{input_field[:type][:name]}`"
+ end
- type = schema.types[type_name]
- return unless type && type.kind.fields?
+ def deprecations
+ strong_memoize(:deprecations) do
+ mapping = {}
- field = type.fields[field_name]
- return field if arg_name.nil?
+ schema.types.each do |type_name, type|
+ next unless type.kind.fields?
- args = field.arguments
- is_mutation = field.mutation && field.mutation <= ::Mutations::BaseMutation
- args = args['input'].type.unwrap.arguments if is_mutation
+ type.fields.each do |field_name, field|
+ mapping["#{type_name}.#{field_name}"] = field.try(:deprecation)
+ field.arguments.each do |arg_name, arg|
+ mapping["#{type_name}.#{field_name}.#{arg_name}"] = arg.try(:deprecation)
+ end
+ end
+ end
+
+ mapping.compact
+ end
+ end
- args[arg_name]
+ def assert!(claim, message)
+ raise ViolatedAssumption, "#{message}\n#{SUGGESTED_ACTION}" unless claim
end
end
end
diff --git a/lib/gitlab/graphql/docs/renderer.rb b/lib/gitlab/graphql/docs/renderer.rb
index 497567f9389..ae0898e6198 100644
--- a/lib/gitlab/graphql/docs/renderer.rb
+++ b/lib/gitlab/graphql/docs/renderer.rb
@@ -24,6 +24,7 @@ module Gitlab
@layout = Haml::Engine.new(File.read(template))
@parsed_schema = GraphQLDocs::Parser.new(schema.graphql_definition, {}).parse
@schema = schema
+ @seen = Set.new
end
def contents
@@ -37,6 +38,16 @@ module Gitlab
FileUtils.mkdir_p(@output_dir)
File.write(filename, contents)
end
+
+ private
+
+ def seen_type?(name)
+ @seen.include?(name)
+ end
+
+ def seen_type!(name)
+ @seen << name
+ end
end
end
end
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index fe73297d0d9..57f4409d76f 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -26,17 +26,81 @@
The `Query` type contains the API's top-level entry points for all executable queries.
\
-- sorted_by_name(queries).each do |query|
- = render_name_and_description(query, owner: 'Query')
- \
- = render_return_type(query)
- - unless query[:arguments].empty?
- ~ "#### Arguments\n"
- ~ "| Name | Type | Description |"
- ~ "| ---- | ---- | ----------- |"
- - sorted_by_name(query[:arguments]).each do |argument|
- = render_field(argument, query[:type][:name])
- \
+- fields_of('Query').each do |field|
+ = render_full_field(field, heading_level: 3, owner: 'Query')
+ \
+
+:plain
+ ## `Mutation` type
+
+ The `Mutation` type contains all the mutations you can execute.
+
+ All mutations receive their arguments in a single input object named `input`, and all mutations
+ support at least a return field `errors` containing a list of error messages.
+
+ All input objects may have a `clientMutationId: String` field, identifying the mutation.
+
+ For example:
+
+ ```graphql
+ mutation($id: NoteableID!, $body: String!) {
+ createNote(input: { noteableId: $id, body: $body }) {
+ errors
+ }
+ }
+ ```
+\
+
+- mutations.each do |field|
+ = render_full_field(field, heading_level: 3, owner: 'Mutation')
+ \
+
+:plain
+ ## Connections
+
+ Some types in our schema are `Connection` types - they represent a paginated
+ collection of edges between two nodes in the graph. These follow the
+ [Relay cursor connections specification](https://relay.dev/graphql/connections.htm).
+
+ ### Pagination arguments {#connection-pagination-arguments}
+
+ All connection fields support the following pagination arguments:
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
+ | `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
+ | `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
+ | `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
+
+ Since these arguments are common to all connection fields, they are not repeated for each connection.
+
+ ### Connection fields
+
+ All connections have at least the following fields:
+
+ | Name | Type | Description |
+ |------|------|-------------|
+ | `pageInfo` | [`PageInfo!`](#pageinfo) | Pagination information. |
+ | `edges` | `[edge!]` | The edges. |
+ | `nodes` | `[item!]` | The items in the current page. |
+
+ The precise type of `Edge` and `Item` depends on the kind of connection. A
+ [`UserConnection`](#userconnection) will have nodes that have the type
+ [`[User!]`](#user), and edges that have the type [`UserEdge`](#useredge).
+
+ ### Connection types
+
+ Some of the types in the schema exist solely to model connections. Each connection
+ has a distinct, named type, with a distinct named edge type. These are listed separately
+ below.
+\
+
+- connection_object_types.each do |type|
+ = render_name_and_description(type, level: 4)
+ \
+ = render_object_fields(type[:fields], owner: type, level_bump: 1)
+ \
:plain
## Object types
@@ -44,22 +108,20 @@
Object types represent the resources that the GitLab GraphQL API can return.
They contain _fields_. Each field has its own type, which will either be one of the
basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types)
- (e.g.: `String` or `Boolean`) or other object types.
+ (e.g.: `String` or `Boolean`) or other object types. Fields may have arguments.
+ Fields with arguments are exactly like top-level queries, and are listed beneath
+ the table of fields for each object type.
For more information, see
[Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields)
on `graphql.org`.
\
-- objects.each do |type|
- - unless type[:fields].empty?
- = render_name_and_description(type)
- \
- ~ "| Field | Type | Description |"
- ~ "| ----- | ---- | ----------- |"
- - sorted_by_name(type[:fields]).each do |field|
- = render_field(field, type[:name])
- \
+- object_types.each do |type|
+ = render_name_and_description(type)
+ \
+ = render_object_fields(type[:fields], owner: type)
+ \
:plain
## Enumeration types
@@ -73,14 +135,13 @@
\
- enums.each do |enum|
- - unless enum[:values].empty?
- = render_name_and_description(enum)
- \
- ~ "| Value | Description |"
- ~ "| ----- | ----------- |"
- - sorted_by_name(enum[:values]).each do |value|
- = render_enum_value(enum, value)
- \
+ = render_name_and_description(enum)
+ \
+ ~ "| Value | Description |"
+ ~ "| ----- | ----------- |"
+ - enum[:values].each do |value|
+ = render_enum_value(enum, value)
+ \
:plain
## Scalar types
@@ -133,7 +194,7 @@
### Interfaces
\
-- graphql_interface_types.each do |type|
+- interfaces.each do |type|
= render_name_and_description(type, level: 4)
\
Implementations:
@@ -141,8 +202,21 @@
- type[:implemented_by].each do |type_name|
~ "- [`#{type_name}`](##{type_name.downcase})"
\
- ~ "| Field | Type | Description |"
- ~ "| ----- | ---- | ----------- |"
- - sorted_by_name(type[:fields] + type[:connections]).each do |field|
- = render_field(field, type[:name])
+ = render_object_fields(type[:fields], owner: type, level_bump: 1)
+ \
+
+:plain
+ ## Input types
+
+ Types that may be used as arguments (all scalar types may also
+ be used as arguments).
+
+ Only general use input types are listed here. For mutation input types,
+ see the associated mutation type above.
+\
+
+- input_types.each do |type|
+ = render_name_and_description(type)
+ \
+ = render_argument_table(3, type[:input_fields], type[:name])
\