Commit a07fe9d7 authored by Alex Ives's avatar Alex Ives

Fixes #29385: Add /shrug and /tableflip commands

- Updated DSL to support substitution definitions
- Added substitution definition, inherits from command definition
- Added tabelflip and shrug substitutions to interpret service
- Added support for substitution definitions to the extractor for preview mode.
- Added substitution handling in the interpret service
Signed-off-by: default avatarAlex Ives <alex@ives.mn>
parent da967803
...@@ -4,6 +4,9 @@ module QuickActions ...@@ -4,6 +4,9 @@ module QuickActions
attr_reader :issuable attr_reader :issuable
SHRUG = '¯\\_(ツ)_/¯'.freeze
TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze
# Takes a text and interprets the commands that are extracted from it. # Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record. # Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable) def execute(content, issuable)
...@@ -14,6 +17,7 @@ module QuickActions ...@@ -14,6 +17,7 @@ module QuickActions
content, commands = extractor.extract_commands(content, context) content, commands = extractor.extract_commands(content, context)
extract_updates(commands, context) extract_updates(commands, context)
[content, @updates] [content, @updates]
end end
...@@ -423,6 +427,18 @@ module QuickActions ...@@ -423,6 +427,18 @@ module QuickActions
@updates[:spend_time] = { duration: :reset, user: current_user } @updates[:spend_time] = { duration: :reset, user: current_user }
end end
desc "Append the comment with #{SHRUG}"
params '<Comment>'
substitution :shrug do |comment|
"#{comment} #{SHRUG}"
end
desc "Append the comment with #{TABLEFLIP}"
params '<Comment>'
substitution :tableflip do |comment|
"#{comment} #{TABLEFLIP}"
end
# This is a dummy command, so that it appears in the autocomplete commands # This is a dummy command, so that it appears in the autocomplete commands
desc 'CC' desc 'CC'
params '@user' params '@user'
......
---
title: Add /shrug and /tableflip commands
merge_request: 10068
author: Alex Ives
...@@ -105,9 +105,32 @@ module Gitlab ...@@ -105,9 +105,32 @@ module Gitlab
# # Awesome code block # # Awesome code block
# end # end
def command(*command_names, &block) def command(*command_names, &block)
define_command(CommandDefinition, *command_names, &block)
end
# Registers a new substitution which is recognizable from body of email or
# comment.
# It accepts aliases and takes a block with the formatted content.
#
# Example:
#
# command :my_substitution, :alias_for_my_substitution do |text|
# "#{text} MY AWESOME SUBSTITUTION"
# end
def substitution(*substitution_names, &block)
define_command(SubstitutionDefinition, *substitution_names, &block)
end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end
private
def define_command(klass, *command_names, &block)
name, *aliases = command_names name, *aliases = command_names
definition = CommandDefinition.new( definition = klass.new(
name, name,
aliases: aliases, aliases: aliases,
description: @description, description: @description,
...@@ -130,10 +153,6 @@ module Gitlab ...@@ -130,10 +153,6 @@ module Gitlab
@condition_block = nil @condition_block = nil
@parse_params_block = nil @parse_params_block = nil
end end
def definition_by_name(name)
command_definitions_by_name[name.to_sym]
end
end end
end end
end end
......
...@@ -46,6 +46,8 @@ module Gitlab ...@@ -46,6 +46,8 @@ module Gitlab
end end
end end
content, commands = perform_substitutions(content, commands)
[content.strip, commands] [content.strip, commands]
end end
...@@ -110,6 +112,26 @@ module Gitlab ...@@ -110,6 +112,26 @@ module Gitlab
}mx }mx
end end
def perform_substitutions(content, commands)
return unless content
substitution_definitions = self.command_definitions.select do |definition|
definition.is_a?(Gitlab::QuickActions::SubstitutionDefinition)
end
substitution_definitions.each do |substitution|
match_data = substitution.match(content)
if match_data
command = [substitution.name.to_s]
command << match_data[1] unless match_data[1].empty?
commands << command
end
content = substitution.perform_substitution(self, content)
end
[content, commands]
end
def command_names(opts) def command_names(opts)
command_definitions.flat_map do |command| command_definitions.flat_map do |command|
next if command.noop? next if command.noop?
......
module Gitlab
module QuickActions
class SubstitutionDefinition < CommandDefinition
# noop?=>true means these won't get extracted or removed by Gitlab::QuickActions::Extractor#extract_commands
# QuickActions::InterpretService#perform_substitutions handles them separately
def noop?
true
end
def match(content)
content.match %r{^/#{all_names.join('|')} ?(.*)$}
end
def perform_substitution(context, content)
return unless content
all_names.each do |a_name|
content.gsub!(%r{/#{a_name} ?(.*)$}, execute_block(action_block, context, '\1'))
end
content
end
end
end
end
...@@ -42,13 +42,18 @@ describe Gitlab::QuickActions::Dsl do ...@@ -42,13 +42,18 @@ describe Gitlab::QuickActions::Dsl do
command :with_params_parsing do |parsed| command :with_params_parsing do |parsed|
parsed parsed
end end
params '<Comment>'
substitution :something do |text|
"#{text} Some complicated thing you want in here"
end
end end
end end
describe '.command_definitions' do describe '.command_definitions' do
it 'returns an array with commands definitions' do it 'returns an array with commands definitions' do
no_args_def, explanation_with_aliases_def, dynamic_description_def, no_args_def, explanation_with_aliases_def, dynamic_description_def,
cc_def, cond_action_def, with_params_parsing_def = cc_def, cond_action_def, with_params_parsing_def, substitution_def =
DummyClass.command_definitions DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args) expect(no_args_def.name).to eq(:no_args)
...@@ -104,6 +109,15 @@ describe Gitlab::QuickActions::Dsl do ...@@ -104,6 +109,15 @@ describe Gitlab::QuickActions::Dsl do
expect(with_params_parsing_def.condition_block).to be_nil expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
expect(substitution_def.name).to eq(:something)
expect(substitution_def.aliases).to eq([])
expect(substitution_def.description).to eq('')
expect(substitution_def.explanation).to eq('')
expect(substitution_def.params).to eq(['<Comment>'])
expect(substitution_def.condition_block).to be_nil
expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
expect(substitution_def.parse_params_block).to be_nil
end end
end end
end end
...@@ -9,6 +9,11 @@ describe Gitlab::QuickActions::Extractor do ...@@ -9,6 +9,11 @@ describe Gitlab::QuickActions::Extractor do
command(:assign) { } command(:assign) { }
command(:labels) { } command(:labels) { }
command(:power) { } command(:power) { }
command(:noop_command)
substitution(:substitution) { 'foo' }
substitution :shrug do |comment|
"#{comment} SHRUG"
end
end.command_definitions end.command_definitions
end end
...@@ -177,6 +182,38 @@ describe Gitlab::QuickActions::Extractor do ...@@ -177,6 +182,38 @@ describe Gitlab::QuickActions::Extractor do
expect(msg).to eq "hello\nworld" expect(msg).to eq "hello\nworld"
end end
it 'does not extract noop commands' do
msg = %(hello\nworld\n/reopen\n/noop_command)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen']]
expect(msg).to eq "hello\nworld\n/noop_command"
end
it 'extracts and performs substitution commands' do
msg = %(hello\nworld\n/reopen\n/substitution)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['substitution']]
expect(msg).to eq "hello\nworld\nfoo"
end
it 'extracts and performs substitution commands' do
msg = %(hello\nworld\n/reopen\n/shrug this is great?)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['shrug', 'this is great?']]
expect(msg).to eq "hello\nworld\nthis is great? SHRUG"
end
it 'extracts and performs substitution commands with comments' do
msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.)
msg, commands = extractor.extract_commands(msg)
expect(commands).to eq [['reopen'], ['substitution', 'wow this is a thing.']]
expect(msg).to eq "hello\nworld\nfoo"
end
it 'extracts multiple commands' do it 'extracts multiple commands' do
msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen) msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
msg, commands = extractor.extract_commands(msg) msg, commands = extractor.extract_commands(msg)
......
require 'spec_helper'
describe Gitlab::QuickActions::SubstitutionDefinition do
let(:content) do
<<EOF
Hello! Let's do this!
/sub_name I like this stuff
EOF
end
subject do
described_class.new(:sub_name, action_block: proc { |text| "#{text} foo" })
end
describe '#perform_substitution!' do
it 'returns nil if content is nil' do
expect(subject.perform_substitution(self, nil)).to be_nil
end
it 'performs the substitution by default' do
expect(subject.perform_substitution(self, content)).to eq <<EOF
Hello! Let's do this!
I like this stuff foo
EOF
end
end
describe '#match' do
it 'checks the content for the command' do
expect(subject.match(content)).to be_truthy
end
it 'returns the match data' do
data = subject.match(content)
expect(data).to be_a(MatchData)
expect(data[1]).to eq('I like this stuff')
end
it 'is nil if content does not have the command' do
expect(subject.match('blah')).to be_falsey
end
end
end
...@@ -9,13 +9,13 @@ describe QuickActions::InterpretService do ...@@ -9,13 +9,13 @@ describe QuickActions::InterpretService do
let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') } let(:bug) { create(:label, project: project, title: 'Bug') }
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
let(:service) { described_class.new(project, developer) }
before do before do
project.team << [developer, :developer] project.team << [developer, :developer]
end end
describe '#execute' do describe '#execute' do
let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
shared_examples 'reopen command' do shared_examples 'reopen command' do
...@@ -270,6 +270,22 @@ describe QuickActions::InterpretService do ...@@ -270,6 +270,22 @@ describe QuickActions::InterpretService do
end end
end end
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
expect(new_content).to end_with(described_class::SHRUG)
end
end
shared_examples 'tableflip command' do
it 'appends (╯°□°)╯︵ ┻━┻ to the comment' do
new_content, _ = service.execute(content, issuable)
expect(new_content).to end_with(described_class::TABLEFLIP)
end
end
it_behaves_like 'reopen command' do it_behaves_like 'reopen command' do
let(:content) { '/reopen' } let(:content) { '/reopen' }
let(:issuable) { issue } let(:issuable) { issue }
...@@ -775,6 +791,30 @@ describe QuickActions::InterpretService do ...@@ -775,6 +791,30 @@ describe QuickActions::InterpretService do
end end
end end
context '/shrug command' do
it_behaves_like 'shrug command' do
let(:content) { '/shrug people are people' }
let(:issuable) { issue }
end
it_behaves_like 'shrug command' do
let(:content) { '/shrug' }
let(:issuable) { issue }
end
end
context '/tableflip command' do
it_behaves_like 'tableflip command' do
let(:content) { '/tableflip curse your sudden but enviable betrayal' }
let(:issuable) { issue }
end
it_behaves_like 'tableflip command' do
let(:content) { '/tableflip' }
let(:issuable) { issue }
end
end
context '/target_branch command' do context '/target_branch command' do
let(:non_empty_project) { create(:project, :repository) } let(:non_empty_project) { create(:project, :repository) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment