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: Alex Ives <alex@ives.mn>
This commit is contained in:
parent
da967803cc
commit
a07fe9d7f8
|
|
@ -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
|
||||||
|
|
||||||
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
|
||||||
@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'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add /shrug and /tableflip commands
|
||||||
|
merge_request: 10068
|
||||||
|
author: Alex Ives
|
||||||
|
|
@ -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
|
||||||
@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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
content, commands = perform_substitutions(content, commands)
|
||||||
|
|
||||||
[content.strip, commands]
|
[content.strip, commands]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
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) }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue