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 | ||||
| 
 | ||||
|     SHRUG = '¯\\_(ツ)_/¯'.freeze | ||||
|     TABLEFLIP = '(╯°□°)╯︵ ┻━┻'.freeze | ||||
| 
 | ||||
|     # 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. | ||||
|     def execute(content, issuable) | ||||
|  | @ -14,6 +17,7 @@ module QuickActions | |||
| 
 | ||||
|       content, commands = extractor.extract_commands(content, context) | ||||
|       extract_updates(commands, context) | ||||
| 
 | ||||
|       [content, @updates] | ||||
|     end | ||||
| 
 | ||||
|  | @ -423,6 +427,18 @@ module QuickActions | |||
|       @updates[:spend_time] = { duration: :reset, user: current_user } | ||||
|     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 | ||||
|     desc 'CC' | ||||
|     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 | ||||
|         #   end | ||||
|         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 | ||||
| 
 | ||||
|           definition = CommandDefinition.new( | ||||
|           definition = klass.new( | ||||
|             name, | ||||
|             aliases: aliases, | ||||
|             description: @description, | ||||
|  | @ -130,10 +153,6 @@ module Gitlab | |||
|           @condition_block = nil | ||||
|           @parse_params_block = nil | ||||
|         end | ||||
| 
 | ||||
|         def definition_by_name(name) | ||||
|           command_definitions_by_name[name.to_sym] | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -46,6 +46,8 @@ module Gitlab | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         content, commands = perform_substitutions(content, commands) | ||||
| 
 | ||||
|         [content.strip, commands] | ||||
|       end | ||||
| 
 | ||||
|  | @ -110,6 +112,26 @@ module Gitlab | |||
|         }mx | ||||
|       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) | ||||
|         command_definitions.flat_map do |command| | ||||
|           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| | ||||
|         parsed | ||||
|       end | ||||
| 
 | ||||
|       params '<Comment>' | ||||
|       substitution :something do |text| | ||||
|         "#{text} Some complicated thing you want in here" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.command_definitions' do | ||||
|     it 'returns an array with commands definitions' do | ||||
|       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 | ||||
| 
 | ||||
|       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.action_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 | ||||
|  |  | |||
|  | @ -9,6 +9,11 @@ describe Gitlab::QuickActions::Extractor do | |||
|       command(:assign) { } | ||||
|       command(:labels) { } | ||||
|       command(:power) { } | ||||
|       command(:noop_command) | ||||
|       substitution(:substitution) { 'foo' } | ||||
|       substitution :shrug do |comment| | ||||
|         "#{comment} SHRUG" | ||||
|       end | ||||
|     end.command_definitions | ||||
|   end | ||||
| 
 | ||||
|  | @ -177,6 +182,38 @@ describe Gitlab::QuickActions::Extractor do | |||
|       expect(msg).to eq "hello\nworld" | ||||
|     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 | ||||
|       msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen) | ||||
|       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(:bug) { create(:label, project: project, title: 'Bug') } | ||||
|   let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } | ||||
|   let(:service) { described_class.new(project, developer) } | ||||
| 
 | ||||
|   before do | ||||
|     project.team << [developer, :developer] | ||||
|   end | ||||
| 
 | ||||
|   describe '#execute' do | ||||
|     let(:service) { described_class.new(project, developer) } | ||||
|     let(:merge_request) { create(:merge_request, source_project: project) } | ||||
| 
 | ||||
|     shared_examples 'reopen command' do | ||||
|  | @ -270,6 +270,22 @@ describe QuickActions::InterpretService do | |||
|       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 | ||||
|       let(:content) { '/reopen' } | ||||
|       let(:issuable) { issue } | ||||
|  | @ -775,6 +791,30 @@ describe QuickActions::InterpretService do | |||
|       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 | ||||
|       let(:non_empty_project) { create(:project, :repository) } | ||||
|       let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue