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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-17 15:10:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-17 15:10:02 +0300
commit11cb5f046dddc630abd416593e176d65f6ba2b69 (patch)
tree01019213f9ea4a50fa5b7c7593e98570f6fa5c69 /lib/gitlab/changelog
parent612bb6f624ea7fdf5fd20e3332d543191603db88 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/changelog')
-rw-r--r--lib/gitlab/changelog/ast.rb157
-rw-r--r--lib/gitlab/changelog/config.rb14
-rw-r--r--lib/gitlab/changelog/eval_state.rb26
-rw-r--r--lib/gitlab/changelog/parser.rb176
-rw-r--r--lib/gitlab/changelog/release.rb9
5 files changed, 19 insertions, 363 deletions
diff --git a/lib/gitlab/changelog/ast.rb b/lib/gitlab/changelog/ast.rb
deleted file mode 100644
index 2c787d396f5..00000000000
--- a/lib/gitlab/changelog/ast.rb
+++ /dev/null
@@ -1,157 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Changelog
- # AST nodes to evaluate when rendering a template.
- #
- # Evaluating an AST is done by walking over the nodes and calling
- # `evaluate`. This method takes two arguments:
- #
- # 1. An instance of `EvalState`, used for tracking data such as the number
- # of nested loops.
- # 2. An object used as the data for the current scope. This can be an Array,
- # Hash, String, or something else. It's up to the AST node to determine
- # what to do with it.
- #
- # While tree walking interpreters (such as implemented here) aren't usually
- # the fastest type of interpreter, they are:
- #
- # 1. Fast enough for our use case
- # 2. Easy to implement and maintain
- #
- # In addition, our AST interpreter doesn't allow for arbitrary code
- # execution, unlike existing template engines such as Mustache
- # (https://github.com/mustache/mustache/issues/244) or ERB.
- #
- # Our interpreter also takes care of limiting the number of nested loops.
- # And unlike Liquid, our interpreter is much smaller and thus has a smaller
- # attack surface. Liquid isn't without its share of issues, such as
- # https://github.com/Shopify/liquid/pull/1071.
- #
- # We also evaluated using Handlebars using the project
- # https://github.com/SmartBear/ruby-handlebars. Sadly, this implementation
- # of Handlebars doesn't support control of whitespace
- # (https://github.com/SmartBear/ruby-handlebars/issues/37), and the project
- # didn't appear to be maintained that much.
- #
- # This doesn't mean these template engines aren't good, instead it means
- # they won't work for our use case. For more information, refer to the
- # comment https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50063#note_469293322.
- module AST
- # An identifier in a selector.
- Identifier = Struct.new(:name) do
- def evaluate(state, data)
- return data if name == 'it'
-
- data[name] if data.is_a?(Hash)
- end
- end
-
- # An integer used in a selector.
- Integer = Struct.new(:value) do
- def evaluate(state, data)
- data[value] if data.is_a?(Array)
- end
- end
-
- # A selector used for loading a value.
- Selector = Struct.new(:steps) do
- def evaluate(state, data)
- steps.reduce(data) do |current, step|
- break if current.nil?
-
- step.evaluate(state, current)
- end
- end
- end
-
- # A tag used for displaying a value in the output.
- Variable = Struct.new(:selector) do
- def evaluate(state, data)
- selector.evaluate(state, data).to_s
- end
- end
-
- # A collection of zero or more expressions.
- Expressions = Struct.new(:nodes) do
- def evaluate(state, data)
- nodes.map { |node| node.evaluate(state, data) }.join('')
- end
- end
-
- # A single text node.
- Text = Struct.new(:text) do
- def evaluate(*)
- text
- end
- end
-
- # An `if` expression, with an optional `else` clause.
- If = Struct.new(:condition, :true_body, :false_body) do
- def evaluate(state, data)
- result =
- if truthy?(condition.evaluate(state, data))
- true_body.evaluate(state, data)
- elsif false_body
- false_body.evaluate(state, data)
- end
-
- result.to_s
- end
-
- def truthy?(value)
- # We treat empty collections and such as false, removing the need for
- # some sort of `if length(x) > 0` expression.
- value.respond_to?(:empty?) ? !value.empty? : !!value
- end
- end
-
- # An `each` expression.
- Each = Struct.new(:collection, :body) do
- def evaluate(state, data)
- values = collection.evaluate(state, data)
-
- return '' unless values.respond_to?(:each)
-
- # While unlikely to happen, it's possible users attempt to nest many
- # loops in order to negatively impact the GitLab instance. To make
- # this more difficult, we limit the number of nested loops a user can
- # create.
- state.enter_loop do
- values.map { |value| body.evaluate(state, value) }.join('')
- end
- end
- end
-
- # A class for transforming a raw Parslet AST into a more structured/easier
- # to work with AST.
- #
- # For more information about Parslet transformations, refer to the
- # documentation at http://kschiess.github.io/parslet/transform.html.
- class Transformer < Parslet::Transform
- rule(ident: simple(:name)) { Identifier.new(name.to_s) }
- rule(int: simple(:name)) { Integer.new(name.to_i) }
- rule(text: simple(:text)) { Text.new(text.to_s) }
- rule(exprs: subtree(:nodes)) { Expressions.new(nodes) }
- rule(selector: sequence(:steps)) { Selector.new(steps) }
- rule(selector: simple(:step)) { Selector.new([step]) }
- rule(variable: simple(:selector)) { Variable.new(selector) }
- rule(each: simple(:values), body: simple(:body)) do
- Each.new(values, body)
- end
-
- rule(if: simple(:cond), true_body: simple(:true_body)) do
- If.new(cond, true_body)
- end
-
- rule(
- if: simple(:cond),
- true_body: simple(:true_body),
- false_body: simple(:false_body)
- ) do
- If.new(cond, true_body, false_body)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/changelog/config.rb b/lib/gitlab/changelog/config.rb
index be8009750da..0538fe68474 100644
--- a/lib/gitlab/changelog/config.rb
+++ b/lib/gitlab/changelog/config.rb
@@ -52,7 +52,12 @@ module Gitlab
end
if (template = hash['template'])
- config.template = Parser.new.parse_and_transform(template)
+ config.template =
+ begin
+ TemplateParser::Parser.new.parse_and_transform(template)
+ rescue TemplateParser::Error => e
+ raise Error, e.message
+ end
end
if (categories = hash['categories'])
@@ -73,7 +78,12 @@ module Gitlab
def initialize(project)
@project = project
@date_format = DEFAULT_DATE_FORMAT
- @template = Parser.new.parse_and_transform(DEFAULT_TEMPLATE)
+ @template =
+ begin
+ TemplateParser::Parser.new.parse_and_transform(DEFAULT_TEMPLATE)
+ rescue TemplateParser::Error => e
+ raise Error, e.message
+ end
@categories = {}
@tag_regex = DEFAULT_TAG_REGEX
end
diff --git a/lib/gitlab/changelog/eval_state.rb b/lib/gitlab/changelog/eval_state.rb
deleted file mode 100644
index a0439df60cf..00000000000
--- a/lib/gitlab/changelog/eval_state.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Changelog
- # A class for tracking state when evaluating a template
- class EvalState
- MAX_LOOPS = 4
-
- def initialize
- @loops = 0
- end
-
- def enter_loop
- if @loops == MAX_LOOPS
- raise Error, "You can only nest up to #{MAX_LOOPS} loops"
- end
-
- @loops += 1
- retval = yield
- @loops -= 1
-
- retval
- end
- end
- end
-end
diff --git a/lib/gitlab/changelog/parser.rb b/lib/gitlab/changelog/parser.rb
deleted file mode 100644
index fac6fc19148..00000000000
--- a/lib/gitlab/changelog/parser.rb
+++ /dev/null
@@ -1,176 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Changelog
- # A parser for the template syntax used for generating changelogs.
- #
- # As a quick primer on the template syntax, a basic template looks like
- # this:
- #
- # {% each users %}
- # Name: {{name}}
- # Age: {{age}}
- #
- # {% if birthday %}
- # This user is celebrating their birthday today! Yay!
- # {% end %}
- # {% end %}
- #
- # For more information, refer to the Parslet documentation found at
- # http://kschiess.github.io/parslet/.
- class Parser < Parslet::Parser
- root(:exprs)
-
- rule(:exprs) do
- (
- variable | if_expr | each_expr | escaped | text | newline
- ).repeat.as(:exprs)
- end
-
- rule(:space) { match('[ \\t]') }
- rule(:whitespace) { match('\s').repeat }
- rule(:lf) { str("\n") }
- rule(:newline) { lf.as(:text) }
-
- # Escaped newlines are ignored, allowing the user to control the
- # whitespace in the output. All other escape sequences are treated as
- # literal text.
- #
- # For example, this:
- #
- # foo \
- # bar
- #
- # Is parsed into this:
- #
- # foo bar
- rule(:escaped) do
- backslash = str('\\')
-
- (backslash >> lf).ignore | (backslash >> chars).as(:text)
- end
-
- # A sequence of regular characters, with the exception of newlines and
- # escaped newlines.
- rule(:chars) do
- char = match("[^{\\\\\n]")
-
- # The rules here are such that we do treat single curly braces or
- # non-opening tags (e.g. `{foo}`) as text, but not opening tags
- # themselves (e.g. `{{`).
- (
- char.repeat(1) | curly_open >> (curly_open | percent).absent?
- ).repeat(1)
- end
-
- rule(:text) { chars.as(:text) }
-
- # An integer, limited to 10 digits (= a 32 bits integer).
- #
- # The size is limited to prevents users from creating integers that are
- # too large, as this may result in runtime errors.
- rule(:integer) { match('\d').repeat(1, 10).as(:int) }
-
- # An identifier to look up in a data structure.
- #
- # We only support simple ASCII identifiers as we simply don't have a need
- # for more complex identifiers (e.g. those containing multibyte
- # characters).
- rule(:ident) { match('[a-zA-Z_]').repeat(1).as(:ident) }
-
- # A selector is used for reading a value, consisting of one or more
- # "steps".
- #
- # Examples:
- #
- # name
- # users.0.name
- # 0
- # it
- rule(:selector) do
- step = ident | integer
-
- whitespace >>
- (step >> (str('.') >> step).repeat).as(:selector) >>
- whitespace
- end
-
- rule(:curly_open) { str('{') }
- rule(:curly_close) { str('}') }
- rule(:percent) { str('%') }
-
- # A variable tag.
- #
- # Examples:
- #
- # {{name}}
- # {{users.0.name}}
- rule(:variable) do
- curly_open.repeat(2) >> selector.as(:variable) >> curly_close.repeat(2)
- end
-
- rule(:expr_open) { curly_open >> percent >> whitespace }
- rule(:expr_close) do
- # Since whitespace control is important (as Markdown is whitespace
- # sensitive), we default to stripping a newline that follows a %} tag.
- # This is less annoying compared to having to opt-in to this behaviour.
- whitespace >> percent >> curly_close >> lf.maybe.ignore
- end
-
- rule(:end_tag) { expr_open >> str('end') >> expr_close }
-
- # An `if` expression, with an optional `else` clause.
- #
- # Examples:
- #
- # {% if foo %}
- # yes
- # {% end %}
- #
- # {% if foo %}
- # yes
- # {% else %}
- # no
- # {% end %}
- rule(:if_expr) do
- else_tag =
- expr_open >> str('else') >> expr_close >> exprs.as(:false_body)
-
- expr_open >>
- str('if') >>
- space.repeat(1) >>
- selector.as(:if) >>
- expr_close >>
- exprs.as(:true_body) >>
- else_tag.maybe >>
- end_tag
- end
-
- # An `each` expression, used for iterating over collections.
- #
- # Example:
- #
- # {% each users %}
- # * {{name}}
- # {% end %}
- rule(:each_expr) do
- expr_open >>
- str('each') >>
- space.repeat(1) >>
- selector.as(:each) >>
- expr_close >>
- exprs.as(:body) >>
- end_tag
- end
-
- def parse_and_transform(input)
- AST::Transformer.new.apply(parse(input))
- rescue Parslet::ParseFailed => ex
- # We raise a custom error so it's easier to catch different changelog
- # related errors. In addition, this ensures the caller of this method
- # doesn't depend on a Parslet specific error class.
- raise Error, "Failed to parse the template: #{ex.message}"
- end
- end
- end
-end
diff --git a/lib/gitlab/changelog/release.rb b/lib/gitlab/changelog/release.rb
index f2a01c2b0dc..c0b6a5c5679 100644
--- a/lib/gitlab/changelog/release.rb
+++ b/lib/gitlab/changelog/release.rb
@@ -54,7 +54,7 @@ module Gitlab
end
def to_markdown
- state = EvalState.new
+ state = TemplateParser::EvalState.new
data = { 'categories' => entries_for_template }
# While not critical, we would like release sections to be separated by
@@ -63,7 +63,12 @@ module Gitlab
#
# Since it can be a bit tricky to get this right in a template, we
# enforce an empty line separator ourselves.
- markdown = @config.template.evaluate(state, data).strip
+ markdown =
+ begin
+ @config.template.evaluate(state, data).strip
+ rescue TemplateParser::ParseError => e
+ raise Error, e.message
+ end
# The release header can't be changed using the Liquid template, as we
# need this to be in a known format. Without this restriction, we won't