Commit 1f5815f5 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'ee/master' into 2302-environment-specific-variables

* ee/master:
  Port of mk-add-project-moved-errors-for-git to EE
  Find first td within the table before assertion
  Add namespace license checks for Service Desk (EEP)
  set ENV['prometheus_multiproc_dir'] in config/boot.rb instead of config.ru
  Use `destroy` instead of `delete` so callbacks are triggered.
  Rename "Slash commands" to "Quick actions"
  Don't track historical data when there isn't a license or it's trial
parents c3c9032b 45efbff1
...@@ -34,7 +34,7 @@ class GfmAutoComplete { ...@@ -34,7 +34,7 @@ class GfmAutoComplete {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
// This triggers at.js again // This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~) // Needed for quick actions with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
$input.on('clear-commands-cache.atwho', () => this.clearCache()); $input.on('clear-commands-cache.atwho', () => this.clearCache());
}); });
...@@ -48,8 +48,8 @@ class GfmAutoComplete { ...@@ -48,8 +48,8 @@ class GfmAutoComplete {
if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
if (this.enableMap.labels) this.setupLabels($input); if (this.enableMap.labels) this.setupLabels($input);
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms
$input.filter('[data-supports-slash-commands="true"]').atwho({ $input.filter('[data-supports-quick-actions="true"]').atwho({
at: '/', at: '/',
alias: 'commands', alias: 'commands',
searchKey: 'search', searchKey: 'search',
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
<textarea <textarea
id="issue-description" id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
data-supports-slash-commands="false" data-supports-quick-actionss="false"
aria-label="Description" aria-label="Description"
v-model="formState.description" v-model="formState.description"
ref="textarea" ref="textarea"
......
...@@ -32,7 +32,7 @@ const normalizeNewlines = function(str) { ...@@ -32,7 +32,7 @@ const normalizeNewlines = function(str) {
(function() { (function() {
this.Notes = (function() { this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_SLASH_COMMANDS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
Notes.interval = null; Notes.interval = null;
...@@ -280,7 +280,7 @@ const normalizeNewlines = function(str) { ...@@ -280,7 +280,7 @@ const normalizeNewlines = function(str) {
return this.initRefresh(); return this.initRefresh();
}; };
Notes.prototype.handleSlashCommands = function(noteEntity) { Notes.prototype.handleQuickActions = function(noteEntity) {
var votesBlock; var votesBlock;
if (noteEntity.commands_changes) { if (noteEntity.commands_changes) {
if ('merge' in noteEntity.commands_changes) { if ('merge' in noteEntity.commands_changes) {
...@@ -1198,27 +1198,27 @@ const normalizeNewlines = function(str) { ...@@ -1198,27 +1198,27 @@ const normalizeNewlines = function(str) {
}; };
/** /**
* Identify if comment has any slash commands * Identify if comment has any quick actions
*/ */
Notes.prototype.hasSlashCommands = function(formContent) { Notes.prototype.hasQuickActions = function(formContent) {
return REGEX_SLASH_COMMANDS.test(formContent); return REGEX_QUICK_ACTIONS.test(formContent);
}; };
/** /**
* Remove slash commands and leave comment with pure message * Remove quick actions and leave comment with pure message
*/ */
Notes.prototype.stripSlashCommands = function(formContent) { Notes.prototype.stripQuickActions = function(formContent) {
return formContent.replace(REGEX_SLASH_COMMANDS, '').trim(); return formContent.replace(REGEX_QUICK_ACTIONS, '').trim();
}; };
/** /**
* Gets appropriate description from slash commands found in provided `formContent` * Gets appropriate description from quick actions found in provided `formContent`
*/ */
Notes.prototype.getSlashCommandDescription = function (formContent, availableSlashCommands = []) { Notes.prototype.getQuickActionDescription = function (formContent, availableQuickActions = []) {
let tempFormContent; let tempFormContent;
// Identify executed slash commands from `formContent` // Identify executed quick actions from `formContent`
const executedCommands = availableSlashCommands.filter((command, index) => { const executedCommands = availableQuickActions.filter((command, index) => {
const commandRegex = new RegExp(`/${command.name}`); const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(formContent); return commandRegex.test(formContent);
}); });
...@@ -1276,7 +1276,7 @@ const normalizeNewlines = function(str) { ...@@ -1276,7 +1276,7 @@ const normalizeNewlines = function(str) {
}; };
/** /**
* Create Placeholder System Note DOM element populated with slash command description * Create Placeholder System Note DOM element populated with quick action description
*/ */
Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) {
const $tempNote = $( const $tempNote = $(
...@@ -1325,7 +1325,7 @@ const normalizeNewlines = function(str) { ...@@ -1325,7 +1325,7 @@ const normalizeNewlines = function(str) {
const { formData, formContent, formAction } = this.getFormData($form); const { formData, formContent, formAction } = this.getFormData($form);
let noteUniqueId; let noteUniqueId;
let systemNoteUniqueId; let systemNoteUniqueId;
let hasSlashCommands = false; let hasQuickActions = false;
let $notesContainer; let $notesContainer;
let tempFormContent; let tempFormContent;
...@@ -1344,9 +1344,9 @@ const normalizeNewlines = function(str) { ...@@ -1344,9 +1344,9 @@ const normalizeNewlines = function(str) {
} }
tempFormContent = formContent; tempFormContent = formContent;
if (this.hasSlashCommands(formContent)) { if (this.hasQuickActions(formContent)) {
tempFormContent = this.stripSlashCommands(formContent); tempFormContent = this.stripQuickActions(formContent);
hasSlashCommands = true; hasQuickActions = true;
} }
// Show placeholder note // Show placeholder note
...@@ -1363,10 +1363,10 @@ const normalizeNewlines = function(str) { ...@@ -1363,10 +1363,10 @@ const normalizeNewlines = function(str) {
} }
// Show placeholder system note // Show placeholder system note
if (hasSlashCommands) { if (hasQuickActions) {
systemNoteUniqueId = _.uniqueId('tempSystemNote_'); systemNoteUniqueId = _.uniqueId('tempSystemNote_');
$notesContainer.append(this.createPlaceholderSystemNote({ $notesContainer.append(this.createPlaceholderSystemNote({
formContent: this.getSlashCommandDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)),
uniqueId: systemNoteUniqueId, uniqueId: systemNoteUniqueId,
})); }));
} }
...@@ -1388,7 +1388,7 @@ const normalizeNewlines = function(str) { ...@@ -1388,7 +1388,7 @@ const normalizeNewlines = function(str) {
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
// Reset cached commands list when command is applied // Reset cached commands list when command is applied
if (hasSlashCommands) { if (hasQuickActions) {
$form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
} }
...@@ -1422,7 +1422,7 @@ const normalizeNewlines = function(str) { ...@@ -1422,7 +1422,7 @@ const normalizeNewlines = function(str) {
} }
if (note.commands_changes) { if (note.commands_changes) {
this.handleSlashCommands(note); this.handleQuickActions(note);
} }
$form.trigger('ajax:success', [note]); $form.trigger('ajax:success', [note]);
...@@ -1430,7 +1430,7 @@ const normalizeNewlines = function(str) { ...@@ -1430,7 +1430,7 @@ const normalizeNewlines = function(str) {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
if (hasSlashCommands) { if (hasQuickActions) {
$notesContainer.find(`#${systemNoteUniqueId}`).remove(); $notesContainer.find(`#${systemNoteUniqueId}`).remove();
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview
// (including the explanation of slash commands), and showing a warning when // (including the explanation of quick actions), and showing a warning when
// more than `x` users are referenced. // more than `x` users are referenced.
// //
(function () { (function () {
......
...@@ -15,10 +15,10 @@ export default { ...@@ -15,10 +15,10 @@ export default {
<div class="time-tracking-help-state"> <div class="time-tracking-help-state">
<div class="time-tracking-info"> <div class="time-tracking-info">
<h4> <h4>
Track time with slash commands Track time with quick actions
</h4> </h4>
<p> <p>
Slash commands can be used in the issues description and comment boxes. Quick actions can be used in the issues description and comment boxes.
</p> </p>
<p> <p>
<code> <code>
......
...@@ -16,10 +16,10 @@ export default { ...@@ -16,10 +16,10 @@ export default {
'issuable-time-tracker': timeTracker, 'issuable-time-tracker': timeTracker,
}, },
methods: { methods: {
listenForSlashCommands() { listenForQuickActions() {
$(document).on('ajax:success', '.gfm-form', this.slashCommandListened); $(document).on('ajax:success', '.gfm-form', this.quickActionListened);
}, },
slashCommandListened(e, data) { quickActionListened(e, data) {
const subscribedCommands = ['spend_time', 'time_estimate']; const subscribedCommands = ['spend_time', 'time_estimate'];
let changedCommands; let changedCommands;
if (data !== undefined) { if (data !== undefined) {
...@@ -35,7 +35,7 @@ export default { ...@@ -35,7 +35,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.listenForSlashCommands(); this.listenForQuickActions();
}, },
template: ` template: `
<div class="block"> <div class="block">
......
...@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper include KerberosSpnegoHelper
attr_reader :authentication_result attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
...@@ -14,7 +14,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -14,7 +14,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
skip_before_action :repository skip_before_action :repository
before_action :authenticate_user before_action :authenticate_user
before_action :ensure_project_found!
private private
...@@ -68,38 +67,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -68,38 +67,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
end end
def ensure_project_found!
render_not_found if project.blank?
end
def project def project
return @project if defined?(@project) parse_repo_path unless defined?(@project)
project_id, _ = project_id_with_suffix
@project =
if project_id.blank?
nil
else
Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
end
end
# This method returns two values so that we can parse @project
# params[:project_id] (untrusted input!) in exactly one place. end
def project_id_with_suffix
id = params[:project_id] || ''
%w[.wiki.git .git].each do |suffix|
if id.end_with?(suffix)
# Be careful to only remove the suffix from the end of 'id'.
# Accidentally removing it from the middle is how security
# vulnerabilities happen!
return [id.slice(0, id.length - suffix.length), suffix]
end
end
# Something is wrong with params[:project_id]; do not pass it on. def parse_repo_path
[nil, nil] @project, @wiki, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end end
def render_missing_personal_token def render_missing_personal_token
...@@ -114,14 +89,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -114,14 +89,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end end
def wiki? def wiki?
return @wiki if defined?(@wiki) parse_repo_path unless defined?(@wiki)
_, suffix = project_id_with_suffix
@wiki = suffix == '.wiki.git'
end
def render_not_found @wiki
render plain: 'Not Found', status: :not_found
end end
def handle_basic_authentication(login, password) def handle_basic_authentication(login, password)
......
...@@ -56,7 +56,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -56,7 +56,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
def access def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities) @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path)
end end
def access_actor def access_actor
......
...@@ -10,8 +10,8 @@ module NotesHelper ...@@ -10,8 +10,8 @@ module NotesHelper
Ability.can_edit_note?(current_user, note) Ability.can_edit_note?(current_user, note)
end end
def note_supports_slash_commands?(note) def note_supports_quick_actions?(note)
Notes::SlashCommandsService.supported?(note, current_user) Notes::QuickActionsService.supported?(note, current_user)
end end
def noteable_json(noteable) def noteable_json(noteable)
......
...@@ -55,8 +55,13 @@ module EE ...@@ -55,8 +55,13 @@ module EE
end end
end end
def service_desk_enabled
::EE::Gitlab::ServiceDesk.enabled?(project: self) && super
end
alias_method :service_desk_enabled?, :service_desk_enabled
def service_desk_address def service_desk_address
return nil unless service_desk_available? return nil unless service_desk_enabled?
config = ::Gitlab.config.incoming_email config = ::Gitlab.config.incoming_email
wildcard = ::Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER wildcard = ::Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
...@@ -118,7 +123,7 @@ module EE ...@@ -118,7 +123,7 @@ module EE
private private
def licensed_feature_available?(feature) def licensed_feature_available?(feature)
globally_available = License.current&.feature_available?(feature) globally_available = License.feature_available?(feature)
if current_application_settings.should_check_namespace_plan? if current_application_settings.should_check_namespace_plan?
globally_available && globally_available &&
...@@ -131,11 +136,5 @@ module EE ...@@ -131,11 +136,5 @@ module EE
def destroy_mirror_data def destroy_mirror_data
mirror_data.destroy mirror_data.destroy
end end
def service_desk_available?
return @service_desk_available if defined?(@service_desk_available)
@service_desk_available = EE::Gitlab::ServiceDesk.enabled? && service_desk_enabled?
end
end end
end end
...@@ -206,6 +206,10 @@ class License < ActiveRecord::Base ...@@ -206,6 +206,10 @@ class License < ActiveRecord::Base
restricted_attr(:trueup_to)].all?(&:present?) restricted_attr(:trueup_to)].all?(&:present?)
end end
def trial?
restricted_attr(:trial)
end
private private
def restricted_attr(name, default = nil) def restricted_attr(name, default = nil)
......
...@@ -959,7 +959,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -959,7 +959,7 @@ class MergeRequest < ActiveRecord::Base
!has_commits? !has_commits?
end end
def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
return false unless can_be_merged_by?(current_user) return false unless can_be_merged_by?(current_user)
return true if autocomplete_precheck return true if autocomplete_precheck
......
...@@ -33,7 +33,7 @@ class Note < ActiveRecord::Base ...@@ -33,7 +33,7 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer # Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count attr_accessor :user_visible_reference_count
# Attribute used to store the attributes that have ben changed by slash commands. # Attribute used to store the attributes that have ben changed by quick actions.
attr_accessor :commands_changes attr_accessor :commands_changes
default_value_for :system, false default_value_for :system, false
......
class MattermostSlashCommandsService < ChatSlashCommandsService class MattermostSlashCommandsService < SlashCommandsService
include TriggersHelper include TriggersHelper
prop_accessor :token prop_accessor :token
......
class SlackSlashCommandsService < ChatSlashCommandsService class SlackSlashCommandsService < SlashCommandsService
include TriggersHelper include TriggersHelper
def title def title
......
# Base class for Chat services # Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from. # This class is not meant to be used directly, but only to inherrit from.
class ChatSlashCommandsService < Service class SlashCommandsService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
prop_accessor :token prop_accessor :token
...@@ -33,10 +33,10 @@ class ChatSlashCommandsService < Service ...@@ -33,10 +33,10 @@ class ChatSlashCommandsService < Service
user = find_chat_user(params) user = find_chat_user(params)
if user if user
Gitlab::ChatCommands::Command.new(project, user, params).execute Gitlab::SlashCommands::Command.new(project, user, params).execute
else else
url = authorize_chat_name_url(params) url = authorize_chat_name_url(params)
Gitlab::ChatCommands::Presenters::Access.new(url).authorize Gitlab::SlashCommands::Presenters::Access.new(url).authorize
end end
end end
......
...@@ -10,7 +10,7 @@ class GlobalPolicy < BasePolicy ...@@ -10,7 +10,7 @@ class GlobalPolicy < BasePolicy
can! :access_api can! :access_api
can! :access_git can! :access_git
can! :receive_notifications can! :receive_notifications
can! :use_slash_commands can! :use_quick_actions
end end
end end
end end
...@@ -142,9 +142,9 @@ class IssuableBaseService < BaseService ...@@ -142,9 +142,9 @@ class IssuableBaseService < BaseService
LabelsFinder.new(current_user, project_id: @project.id).execute LabelsFinder.new(current_user, project_id: @project.id).execute
end end
def merge_slash_commands_into_params!(issuable) def merge_quick_actions_into_params!(issuable)
description, command_params = description, command_params =
SlashCommands::InterpretService.new(project, current_user). QuickActions::InterpretService.new(project, current_user).
execute(params[:description], issuable) execute(params[:description], issuable)
# Avoid a description already set on an issuable to be overwritten by a nil # Avoid a description already set on an issuable to be overwritten by a nil
...@@ -162,7 +162,7 @@ class IssuableBaseService < BaseService ...@@ -162,7 +162,7 @@ class IssuableBaseService < BaseService
end end
def create(issuable) def create(issuable)
merge_slash_commands_into_params!(issuable) merge_quick_actions_into_params!(issuable)
filter_params(issuable) filter_params(issuable)
params.delete(:state_event) params.delete(:state_event)
......
...@@ -7,7 +7,7 @@ module MergeRequests ...@@ -7,7 +7,7 @@ module MergeRequests
params.except!(:target_project_id) params.except!(:target_project_id)
params.except!(:source_branch) params.except!(:source_branch)
merge_from_slash_command(merge_request) if params[:merge] merge_from_quick_action(merge_request) if params[:merge]
if merge_request.closed_without_fork? if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch) params.except!(:target_branch, :force_remove_source_branch)
...@@ -86,9 +86,9 @@ module MergeRequests ...@@ -86,9 +86,9 @@ module MergeRequests
end end
end end
def merge_from_slash_command(merge_request) def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge) last_diff_sha = params.delete(:merge)
return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha)
merge_request.update(merge_error: nil) merge_request.update(merge_error: nil)
......
...@@ -9,11 +9,11 @@ module Notes ...@@ -9,11 +9,11 @@ module Notes
# We execute commands (extracted from `params[:note]`) on the noteable # We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands # **before** we save the note because if the note consists of commands
# only, there is no need be create a note! # only, there is no need be create a note!
slash_commands_service = SlashCommandsService.new(project, current_user) quick_actions_service = QuickActionsService.new(project, current_user)
if slash_commands_service.supported?(note) if quick_actions_service.supported?(note)
options = { merge_request_diff_head_sha: merge_request_diff_head_sha } options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
content, command_params = slash_commands_service.extract_commands(note, options) content, command_params = quick_actions_service.extract_commands(note, options)
only_commands = content.empty? only_commands = content.empty?
...@@ -30,7 +30,7 @@ module Notes ...@@ -30,7 +30,7 @@ module Notes
end end
if command_params.present? if command_params.present?
slash_commands_service.execute(command_params, note) quick_actions_service.execute(command_params, note)
# We must add the error after we call #save because errors are reset # We must add the error after we call #save because errors are reset
# when #save is called # when #save is called
......
module Notes module Notes
class SlashCommandsService < BaseService class QuickActionsService < BaseService
UPDATE_SERVICES = { UPDATE_SERVICES = {
'Issue' => Issues::UpdateService, 'Issue' => Issues::UpdateService,
'MergeRequest' => MergeRequests::UpdateService 'MergeRequest' => MergeRequests::UpdateService
...@@ -22,7 +22,7 @@ module Notes ...@@ -22,7 +22,7 @@ module Notes
def extract_commands(note, options = {}) def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note) return [note.note, {}] unless supported?(note)
SlashCommands::InterpretService.new(project, current_user, options). QuickActions::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable) execute(note.note, note.noteable)
end end
......
class PreviewMarkdownService < BaseService class PreviewMarkdownService < BaseService
def execute def execute
text, commands = explain_slash_commands(params[:text]) text, commands = explain_quick_actions(params[:text])
users = find_user_references(text) users = find_user_references(text)
success( success(
...@@ -12,11 +12,11 @@ class PreviewMarkdownService < BaseService ...@@ -12,11 +12,11 @@ class PreviewMarkdownService < BaseService
private private
def explain_slash_commands(text) def explain_quick_actions(text)
return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) return text, [] unless %w(Issue MergeRequest).include?(commands_target_type)
slash_commands_service = SlashCommands::InterpretService.new(project, current_user) quick_actions_service = QuickActions::InterpretService.new(project, current_user)
slash_commands_service.explain(text, find_commands_target) quick_actions_service.explain(text, find_commands_target)
end end
def find_user_references(text) def find_user_references(text)
...@@ -36,10 +36,10 @@ class PreviewMarkdownService < BaseService ...@@ -36,10 +36,10 @@ class PreviewMarkdownService < BaseService
end end
def commands_target_type def commands_target_type
params[:slash_commands_target_type] params[:quick_actions_target_type]
end end
def commands_target_id def commands_target_id
params[:slash_commands_target_id] params[:quick_actions_target_id]
end end
end end
...@@ -32,7 +32,7 @@ module Projects ...@@ -32,7 +32,7 @@ module Projects
issuable: noteable, issuable: noteable,
current_user: current_user current_user: current_user
} }
SlashCommands::InterpretService.command_definitions.map do |definition| QuickActions::InterpretService.command_definitions.map do |definition|
next unless definition.available?(opts) next unless definition.available?(opts)
definition.to_h(opts) definition.to_h(opts)
......
module SlashCommands module QuickActions
class InterpretService < BaseService class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl include Gitlab::QuickActions::Dsl
attr_reader :issuable attr_reader :issuable
# 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)
return [content, {}] unless current_user.can?(:use_slash_commands) return [content, {}] unless current_user.can?(:use_quick_actions)
@issuable = issuable @issuable = issuable
@updates = {} @updates = {}
...@@ -20,7 +20,7 @@ module SlashCommands ...@@ -20,7 +20,7 @@ module SlashCommands
# 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 array of changes explained. # Returns the content without commands, and array of changes explained.
def explain(content, issuable) def explain(content, issuable)
return [content, []] unless current_user.can?(:use_slash_commands) return [content, []] unless current_user.can?(:use_quick_actions)
@issuable = issuable @issuable = issuable
...@@ -32,7 +32,7 @@ module SlashCommands ...@@ -32,7 +32,7 @@ module SlashCommands
private private
def extractor def extractor
Gitlab::SlashCommands::Extractor.new(self.class.command_definitions) Gitlab::QuickActions::Extractor.new(self.class.command_definitions)
end end
desc do desc do
...@@ -71,7 +71,7 @@ module SlashCommands ...@@ -71,7 +71,7 @@ module SlashCommands
last_diff_sha = params && params[:merge_request_diff_head_sha] last_diff_sha = params && params[:merge_request_diff_head_sha]
issuable.is_a?(MergeRequest) && issuable.is_a?(MergeRequest) &&
issuable.persisted? && issuable.persisted? &&
issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) issuable.mergeable_with_quick_action?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
end end
command :merge do command :merge do
@updates[:merge] = params[:merge_request_diff_head_sha] @updates[:merge] = params[:merge_request_diff_head_sha]
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
placeholder: admin_namespace_dropdown_label('Search groups'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_available: true} }) placeholder: admin_namespace_dropdown_label('Search groups'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', all_available: true} })
- if @events.present? - if @events.present?
%table.table %table#events-table.table
%thead %thead
%tr %tr
%th Author %th Author
......
- @gfm_form = true - @gfm_form = true
- current_text ||= nil - current_text ||= nil
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
.zen-backdrop .zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area' - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f - if defined?(f) && f
= f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands } = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_quick_actions: supports_quick_actions }
- else - else
= text_area_tag attr, current_text, class: classes, placeholder: placeholder = text_area_tag attr, current_text, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
......
...@@ -130,7 +130,7 @@ ...@@ -130,7 +130,7 @@
= render 'merge_request_settings', form: f = render 'merge_request_settings', form: f
- if EE::Gitlab::ServiceDesk.enabled? - if EE::Gitlab::ServiceDesk.enabled?(project: @project)
%hr %hr
%fieldset.js-service-desk-setting-wrapper.features.append-bottom-default %fieldset.js-service-desk-setting-wrapper.features.append-bottom-default
%h5.prepend-top-0 %h5.prepend-top-0
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
%p %p
The subject will be used as the title of the new issue, and the message will be the description. The subject will be used as the title of the new issue, and the message will be the description.
= link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 = link_to 'Quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
and styling with and styling with
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
are supported. are supported.
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
- model = local_assigns.fetch(:model) - model = local_assigns.fetch(:model)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- supports_slash_commands = model.new_record? - supports_quick_actions = model.new_record?
- if supports_slash_commands - if supports_quick_actions
- preview_url = preview_markdown_path(project, slash_commands_target_type: model.class.name) - preview_url = preview_markdown_path(project, quick_actions_target_type: model.class.name)
- else - else
- preview_url = preview_markdown_path(project) - preview_url = preview_markdown_path(project)
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea', classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.clearfix .clearfix
.error-alert .error-alert
- supports_slash_commands = note_supports_slash_commands?(@note) - supports_quick_actions = note_supports_quick_actions?(@note)
- if supports_slash_commands - if supports_quick_actions
- preview_url = preview_markdown_path(@project, slash_commands_target_type: @note.noteable_type, slash_commands_target_id: @note.noteable_id) - preview_url = preview_markdown_path(@project, quick_actions_target_type: @note.noteable_type, quick_actions_target_id: @note.noteable_id)
- else - else
- preview_url = preview_markdown_path(@project) - preview_url = preview_markdown_path(@project)
...@@ -27,8 +27,8 @@ ...@@ -27,8 +27,8 @@
attr: :note, attr: :note,
classes: 'note-textarea js-note-text', classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.error-alert .error-alert
.note-form-actions.clearfix .note-form-actions.clearfix
......
- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false) - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
.comment-toolbar.clearfix .comment-toolbar.clearfix
.toolbar-text .toolbar-text
= link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1 = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_slash_commands - if supports_quick_actions
and and
= link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1 = link_to 'quick actions', help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
are are
- else - else
is is
......
...@@ -4,6 +4,8 @@ class HistoricalDataWorker ...@@ -4,6 +4,8 @@ class HistoricalDataWorker
def perform def perform
return if Gitlab::Geo.secondary? return if Gitlab::Geo.secondary?
return if License.current.nil? || License.current&.trial?
HistoricalData.track! HistoricalData.track!
end end
end end
---
title: 'Add namespace license checks for Service Desk (EEP)'
merge_request: 2109
author:
---
title: Rename "Slash commands" to "Quick actions" and deprecate "chat commands" in favor
of "slash commands"
merge_request:
author:
...@@ -15,9 +15,6 @@ if defined?(Unicorn) ...@@ -15,9 +15,6 @@ if defined?(Unicorn)
end end
end end
# set default directory for multiproces metrics gathering
ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir'
require ::File.expand_path('../config/environment', __FILE__) require ::File.expand_path('../config/environment', __FILE__)
map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do
......
...@@ -5,6 +5,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) ...@@ -5,6 +5,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
# set default directory for multiproces metrics gathering
ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir'
# Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage # Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage
require 'bootsnap' require 'bootsnap'
Bootsnap.setup( Bootsnap.setup(
......
...@@ -28,7 +28,7 @@ Shortcuts to GitLab's most visited docs: ...@@ -28,7 +28,7 @@ Shortcuts to GitLab's most visited docs:
- [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow. - [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow.
- See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/). - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown). - [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
- [GitLab Slash Commands](user/project/slash_commands.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI. - [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
### User account ### User account
......
...@@ -166,8 +166,8 @@ For instance this kind of thing: ...@@ -166,8 +166,8 @@ For instance this kind of thing:
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea', classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...", placeholder: "Write a comment or drag your files here...",
supports_slash_commands: !issuable.persisted? supports_quick_actions: !issuable.persisted?
= render 'projects/notes/hints', supports_slash_commands: !issuable.persisted? = render 'projects/notes/hints', supports_quick_actions: !issuable.persisted?
.clearfix .clearfix
.error-alert .error-alert
- if issuable.is_a?(Issue) - if issuable.is_a?(Issue)
......
# Chat Commands This document was moved to [integration/slash_commands.md](slash_commands.md).
Chat commands in Mattermost and Slack (also called Slack slash commands) allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it.
Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are:
| Command | Effect |
| ------- | ------ |
| `/project-name help` | Shows all available chat commands |
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
\ No newline at end of file
# Slash Commands
Slash commands in Mattermost and Slack allow you to control GitLab and view GitLab content right inside your chat client, without having to leave it. For Slack, this requires a [project service configuration](../user/project/integrations/slack_slash_commands.md). Simply type the command as a message in your chat client to activate it.
Commands are scoped to a project, with a trigger term that is specified during configuration. (We suggest you use the project name as the trigger term for simplicty and clarity.) Taking the trigger term as `project-name`, the commands are:
| Command | Effect |
| ------- | ------ |
| `/project-name help` | Shows all available slash commands |
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
...@@ -10,7 +10,7 @@ You can leave a comment in the following places: ...@@ -10,7 +10,7 @@ You can leave a comment in the following places:
- commits - commits
- commit diffs - commit diffs
The comment area supports [Markdown] and [slash commands]. One can edit their The comment area supports [Markdown] and [quick actions]. One can edit their
own comment at any time, and anyone with [Master access level][permissions] or own comment at any time, and anyone with [Master access level][permissions] or
higher can also edit a comment made by someone else. higher can also edit a comment made by someone else.
...@@ -146,5 +146,5 @@ comments in greater detail. ...@@ -146,5 +146,5 @@ comments in greater detail.
[discussion-view]: img/discussion_view.png [discussion-view]: img/discussion_view.png
[discussions-resolved]: img/discussions_resolved.png [discussions-resolved]: img/discussions_resolved.png
[markdown]: ../markdown.md [markdown]: ../markdown.md
[slash commands]: ../project/slash_commands.md [quick actions]: ../project/quick_actions.md
[permissions]: ../permissions.md [permissions]: ../permissions.md
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
> Introduced in GitLab 8.15 > Introduced in GitLab 8.15
Slack slash commands (also known as chat commmands) allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab. Slack slash commands allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab.
> Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md). > Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md).
...@@ -20,4 +20,4 @@ Slack slash commands (also known as chat commmands) allow you to control GitLab ...@@ -20,4 +20,4 @@ Slack slash commands (also known as chat commmands) allow you to control GitLab
## Usage ## Usage
You can now use the [Slack slash commands](../../../integration/chat_commands.md). You can now use the [Slack slash commands](../../../integration/slash_commands.md).
\ No newline at end of file
...@@ -68,7 +68,7 @@ This feature is available only in [GitLab Enterprise Edition](https://about.gitl ...@@ -68,7 +68,7 @@ This feature is available only in [GitLab Enterprise Edition](https://about.gitl
- Spend: add the time spent on the implementation of that issue - Spend: add the time spent on the implementation of that issue
> **Note:** > **Note:**
both estimate and spend times are set via [GitLab Slash Commands](../slash_commands.md). both estimate and spend times are set via [GitLab Quick Actions](../quick_actions.md).
Learn more on the [Time Tracking documentation](../../../workflow/time_tracking.md). Learn more on the [Time Tracking documentation](../../../workflow/time_tracking.md).
...@@ -147,7 +147,7 @@ or in the issue thread. ...@@ -147,7 +147,7 @@ or in the issue thread.
#### 15. Award emoji #### 15. Award emoji
- Award an emoji to that issue. - Award an emoji to that issue.
> **Tip:** > **Tip:**
Posting "+1" as comments in threads spam all Posting "+1" as comments in threads spam all
......
# GitLab quick actions
Quick actions are textual shortcuts for common actions on issues or merge
requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
You can enter these commands while creating a new issue or merge request, and
in comments. Each command should be on a separate line in order to be properly
detected and executed. The commands are removed from the issue, merge request or
comment body before it is saved and will not be visible to anyone else.
Below is a list of all of the available commands and descriptions about what they
do.
| Command | Action |
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
| `/merge` | Merge (when pipeline succeeds) |
| `/title <New title>` | Change title |
| `/assign @user1 @user2 ` | Add assignee(s) |
| `/reassign @user1 @user2 ` | Change assignee(s) |
| `/unassign @user1 @user2` | Remove all or specific assignee(s) |
| `/milestone %milestone` | Set milestone |
| `/remove_milestone` | Remove milestone |
| `/label ~foo ~"bar baz"` | Add label(s) |
| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) |
| `/relabel ~foo ~"bar baz"` | Replace all label(s) |
| `/todo` | Add a todo |
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
| `/weight <1-9>` | Set the weight of the issue |
| `/clear_weight` | Clears the issue weight |
| `/board_move ~column` | Move issue to column on the board |
Note: In GitLab EES every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign`
support multiple assignees.
# GitLab slash commands This document was moved to [user/project/quick_actions.md](quick_actions.md).
Slash commands are textual shortcuts for common actions on issues or merge
requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
You can enter these commands while creating a new issue or merge request, and
in comments. Each command should be on a separate line in order to be properly
detected and executed. The commands are removed from the issue, merge request or
comment body before it is saved and will not be visible to anyone else.
Below is a list of all of the available commands and descriptions about what they
do.
| Command | Action |
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
| `/merge` | Merge (when pipeline succeeds) |
| `/title <New title>` | Change title |
| `/assign @user1 @user2 ` | Add assignee(s) |
| `/reassign @user1 @user2 ` | Change assignee(s) |
| `/unassign @user1 @user2` | Remove all or specific assignee(s) |
| `/milestone %milestone` | Set milestone |
| `/remove_milestone` | Remove milestone |
| `/label ~foo ~"bar baz"` | Add label(s) |
| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) |
| `/relabel ~foo ~"bar baz"` | Replace all label(s) |
| `/todo` | Add a todo |
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
| `/remove_time_spent` | Remove time spent |
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
| `/weight <1-9>` | Set the weight of the issue |
| `/clear_weight` | Clears the issue weight |
| `/board_move ~column` | Move issue to column on the board |
Note: In GitLab EES every issue can have more than one assignee, so commands `/assign`, `/unassign` and `/reassign`
support multiple assignees.
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
- [Project users](add-user/add-user.md) - [Project users](add-user/add-user.md)
- [Protected branches](../user/project/protected_branches.md) - [Protected branches](../user/project/protected_branches.md)
- [Protected tags](../user/project/protected_tags.md) - [Protected tags](../user/project/protected_tags.md)
- [Slash commands](../user/project/slash_commands.md) - [Quick Actions](../user/project/quick_actions.md)
- [Sharing a project with a group](share_with_group.md) - [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md) - [Share projects with other groups](share_projects_with_other_groups.md)
- [Time tracking](time_tracking.md) - [Time tracking](time_tracking.md)
......
...@@ -21,13 +21,13 @@ below. ...@@ -21,13 +21,13 @@ below.
## How to enter data ## How to enter data
Time Tracking uses two slash [commands] that are available with this new Time Tracking uses two [quick actions] that GitLab introduced with this new
feature: `/spend` and `/estimate`. feature: `/spend` and `/estimate`.
Slash commands can be used in the body of an issue or a merge request, but also Quick actions can be used in the body of an issue or a merge request, but also
in a comment in both an issue or a merge request. in a comment in both an issue or a merge request.
Below is an example of how you can use those new slash commands inside a comment. Below is an example of how you can use those new quick actions inside a comment.
![Time tracking example in a comment](time-tracking/time-tracking-example.png) ![Time tracking example in a comment](time-tracking/time-tracking-example.png)
...@@ -77,4 +77,4 @@ Other interesting links: ...@@ -77,4 +77,4 @@ Other interesting links:
- [Time Tracking landing page on about.gitlab.com][landing] - [Time Tracking landing page on about.gitlab.com][landing]
[landing]: https://about.gitlab.com/features/time-tracking [landing]: https://about.gitlab.com/features/time-tracking
[commands]: ../user/project/slash_commands.md [quick actions]: ../user/project/quick_actions.md
...@@ -10,6 +10,10 @@ module API ...@@ -10,6 +10,10 @@ module API
set_project unless defined?(@project) set_project unless defined?(@project)
@project @project
end end
def redirected_path
@redirected_path
end
def ssh_authentication_abilities def ssh_authentication_abilities
[ [
...@@ -38,8 +42,9 @@ module API ...@@ -38,8 +42,9 @@ module API
def set_project def set_project
if params[:gl_repository] if params[:gl_repository]
@project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository])
@redirected_path = nil
else else
@project, @wiki = Gitlab::RepoPath.parse(params[:project]) @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project])
end end
end end
......
...@@ -34,7 +34,7 @@ module API ...@@ -34,7 +34,7 @@ module API
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass access_checker = access_checker_klass
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) .new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path)
begin begin
access_checker.check(params[:action], params[:changes]) access_checker.check(params[:action], params[:changes])
......
...@@ -733,7 +733,7 @@ module API ...@@ -733,7 +733,7 @@ module API
trigger_services.each do |service_slug, settings| trigger_services.each do |service_slug, settings|
helpers do helpers do
def chat_command_service(project, service_slug, params) def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service| project.services.active.where(template: false).find do |service|
service.try(:token) == params[:token] && service.to_param == service_slug.underscore service.try(:token) == params[:token] && service.to_param == service_slug.underscore
end end
...@@ -758,7 +758,7 @@ module API ...@@ -758,7 +758,7 @@ module API
# This is not accurate, but done to prevent leakage of the project names # This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project not_found!('Service') unless project
service = chat_command_service(project, service_slug, params) service = slash_command_service(project, service_slug, params)
result = service.try(:trigger, params) result = service.try(:trigger, params)
if result if result
......
...@@ -654,7 +654,7 @@ module API ...@@ -654,7 +654,7 @@ module API
trigger_services.each do |service_slug, settings| trigger_services.each do |service_slug, settings|
helpers do helpers do
def chat_command_service(project, service_slug, params) def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service| project.services.active.where(template: false).find do |service|
service.try(:token) == params[:token] && service.to_param == service_slug.underscore service.try(:token) == params[:token] && service.to_param == service_slug.underscore
end end
...@@ -679,7 +679,7 @@ module API ...@@ -679,7 +679,7 @@ module API
# This is not accurate, but done to prevent leakage of the project names # This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project not_found!('Service') unless project
service = chat_command_service(project, service_slug, params) service = slash_command_service(project, service_slug, params)
result = service.try(:trigger, params) result = service.try(:trigger, params)
if result if result
......
...@@ -145,7 +145,7 @@ module EE ...@@ -145,7 +145,7 @@ module EE
elsif group.last_owner?(user) elsif group.last_owner?(user)
warn_cannot_remove_last_owner(user, group) warn_cannot_remove_last_owner(user, group)
else else
group.users.delete(user) group.users.destroy(user)
end end
end end
end end
......
module EE module EE
module Gitlab module Gitlab
module ServiceDesk module ServiceDesk
def self.enabled? # Check whether a project or GitLab instance can support the Service Desk
::License.current&.feature_available?(:service_desk) && # feature. Use `project.service_desk_enabled?` to check whether it is
::Gitlab::IncomingEmail.enabled? && # enabled for a particular project.
::Gitlab::IncomingEmail.supports_wildcard? def self.enabled?(project: nil)
return unless ::Gitlab::IncomingEmail.enabled? && ::Gitlab::IncomingEmail.supports_wildcard?
(project || ::License).feature_available?(:service_desk)
end end
end end
end end
......
...@@ -31,9 +31,11 @@ module Gitlab ...@@ -31,9 +31,11 @@ module Gitlab
def project def project
return @project if instance_variable_defined?(:@project) return @project if instance_variable_defined?(:@project)
@project = found_project =
Project.where(service_desk_enabled: true) Project.where(service_desk_enabled: true)
.find_by_full_path(service_desk_key) .find_by_full_path(service_desk_key)
@project = found_project&.service_desk_enabled? ? found_project : nil
end end
def create_issue! def create_issue!
......
...@@ -25,12 +25,13 @@ module Gitlab ...@@ -25,12 +25,13 @@ module Gitlab
PUSH_COMMANDS = %w{ git-receive-pack }.freeze PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :authentication_abilities attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path
def initialize(actor, project, protocol, authentication_abilities:) def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil)
@actor = actor @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@redirected_path = redirected_path
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
end end
...@@ -38,6 +39,7 @@ module Gitlab ...@@ -38,6 +39,7 @@ module Gitlab
check_protocol! check_protocol!
check_active_user! check_active_user!
check_project_accessibility! check_project_accessibility!
check_project_moved!
check_command_disabled!(cmd) check_command_disabled!(cmd)
check_command_existence!(cmd) check_command_existence!(cmd)
check_repository_existence! check_repository_existence!
...@@ -92,6 +94,21 @@ module Gitlab ...@@ -92,6 +94,21 @@ module Gitlab
end end
end end
def check_project_moved!
if redirected_path
url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
message = <<-MESSAGE.strip_heredoc
Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote and try again:
git remote set-url origin #{url}
MESSAGE
raise NotFoundError, message
end
end
def check_command_disabled!(cmd) def check_command_disabled!(cmd)
if upload_pack?(cmd) if upload_pack?(cmd)
check_upload_pack_disabled! check_upload_pack_disabled!
......
module Gitlab module Gitlab
module SlashCommands module QuickActions
class CommandDefinition class CommandDefinition
attr_accessor :name, :aliases, :description, :explanation, :params, attr_accessor :name, :aliases, :description, :explanation, :params,
:condition_block, :parse_params_block, :action_block :condition_block, :parse_params_block, :action_block
......
module Gitlab module Gitlab
module SlashCommands module QuickActions
module Dsl module Dsl
extend ActiveSupport::Concern extend ActiveSupport::Concern
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
end end
class_methods do class_methods do
# Allows to give a description to the next slash command. # Allows to give a description to the next quick action.
# This description is shown in the autocomplete menu. # This description is shown in the autocomplete menu.
# It accepts a block that will be evaluated with the context given to # It accepts a block that will be evaluated with the context given to
# `CommandDefintion#to_h`. # `CommandDefintion#to_h`.
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
@description = block_given? ? block : text @description = block_given? ? block : text
end end
# Allows to define params for the next slash command. # Allows to define params for the next quick action.
# These params are shown in the autocomplete menu. # These params are shown in the autocomplete menu.
# #
# Example: # Example:
......
module Gitlab module Gitlab
module SlashCommands module QuickActions
# This class takes an array of commands that should be extracted from a # This class takes an array of commands that should be extracted from a
# given text. # given text.
# #
# ``` # ```
# extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels]) # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
# ``` # ```
class Extractor class Extractor
attr_reader :command_definitions attr_reader :command_definitions
...@@ -24,7 +24,7 @@ module Gitlab ...@@ -24,7 +24,7 @@ module Gitlab
# #
# Usage: # Usage:
# ``` # ```
# extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels]) # extractor = Gitlab::QuickActions::Extractor.new([:open, :assign, :labels])
# msg = %(hello\n/labels ~foo ~"bar baz"\nworld) # msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']] # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld" # msg #=> "hello\nworld"
......
...@@ -3,16 +3,18 @@ module Gitlab ...@@ -3,16 +3,18 @@ module Gitlab
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
def self.parse(repo_path) def self.parse(repo_path)
wiki = false
project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false) project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false)
project = Project.find_by_full_path(project_path) project, was_redirected = find_project(project_path)
if project_path.end_with?('.wiki') && !project
project = Project.find_by_full_path(project_path.chomp('.wiki')) if project_path.end_with?('.wiki') && project.nil?
project, was_redirected = find_project(project_path.chomp('.wiki'))
wiki = true wiki = true
else
wiki = false
end end
[project, wiki] redirected_path = project_path if was_redirected
[project, wiki, redirected_path]
end end
def self.strip_storage_path(repo_path, fail_on_not_found: true) def self.strip_storage_path(repo_path, fail_on_not_found: true)
...@@ -30,5 +32,12 @@ module Gitlab ...@@ -30,5 +32,12 @@ module Gitlab
result.sub(/\A\/*/, '') result.sub(/\A\/*/, '')
end end
def self.find_project(project_path)
project = Project.find_by_full_path(project_path, follow_redirects: true)
was_redirected = project && project.full_path.casecmp(project_path) != 0
[project, was_redirected]
end
end end
end end
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class BaseCommand class BaseCommand
QUERY_LIMIT = 5 QUERY_LIMIT = 5
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class Command < BaseCommand class Command < BaseCommand
COMMANDS = [ COMMANDS = [
Gitlab::ChatCommands::IssueShow, Gitlab::SlashCommands::IssueShow,
Gitlab::ChatCommands::IssueNew, Gitlab::SlashCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch, Gitlab::SlashCommands::IssueSearch,
Gitlab::ChatCommands::Deploy Gitlab::SlashCommands::Deploy
].freeze ].freeze
def execute def execute
...@@ -15,10 +15,10 @@ module Gitlab ...@@ -15,10 +15,10 @@ module Gitlab
if command.allowed?(project, current_user) if command.allowed?(project, current_user)
command.new(project, current_user, params).execute(match) command.new(project, current_user, params).execute(match)
else else
Gitlab::ChatCommands::Presenters::Access.new.access_denied Gitlab::SlashCommands::Presenters::Access.new.access_denied
end end
else else
Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end end
end end
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class Deploy < BaseCommand class Deploy < BaseCommand
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
...@@ -24,12 +24,12 @@ module Gitlab ...@@ -24,12 +24,12 @@ module Gitlab
actions = find_actions(from, to) actions = find_actions(from, to)
if actions.none? if actions.none?
Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions Gitlab::SlashCommands::Presenters::Deploy.new(nil).no_actions
elsif actions.one? elsif actions.one?
action = play!(from, to, actions.first) action = play!(from, to, actions.first)
Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to) Gitlab::SlashCommands::Presenters::Deploy.new(action).present(from, to)
else else
Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions Gitlab::SlashCommands::Presenters::Deploy.new(actions).too_many_actions
end end
end end
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class Help < BaseCommand class Help < BaseCommand
# This class has to be used last, as it always matches. It has to match # This class has to be used last, as it always matches. It has to match
# because other commands were not triggered and we want to show the help # because other commands were not triggered and we want to show the help
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
end end
def execute(commands, text) def execute(commands, text)
Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text) Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, text)
end end
def trigger def trigger
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class IssueCommand < BaseCommand class IssueCommand < BaseCommand
def self.available?(project) def self.available?(project)
project.issues_enabled? && project.default_issues_tracker? project.issues_enabled? && project.default_issues_tracker?
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class IssueNew < IssueCommand class IssueNew < IssueCommand
def self.match(text) def self.match(text)
# we can not match \n with the dot by passing the m modifier as than # we can not match \n with the dot by passing the m modifier as than
...@@ -35,7 +35,7 @@ module Gitlab ...@@ -35,7 +35,7 @@ module Gitlab
end end
def presenter(issue) def presenter(issue)
Gitlab::ChatCommands::Presenters::IssueNew.new(issue) Gitlab::SlashCommands::Presenters::IssueNew.new(issue)
end end
end end
end end
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class IssueSearch < IssueCommand class IssueSearch < IssueCommand
def self.match(text) def self.match(text)
/\Aissue\s+search\s+(?<query>.*)/.match(text) /\Aissue\s+search\s+(?<query>.*)/.match(text)
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
class IssueShow < IssueCommand class IssueShow < IssueCommand
def self.match(text) def self.match(text)
/\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text) /\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text)
...@@ -13,9 +13,9 @@ module Gitlab ...@@ -13,9 +13,9 @@ module Gitlab
issue = find_by_iid(match[:iid]) issue = find_by_iid(match[:iid])
if issue if issue
Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present Gitlab::SlashCommands::Presenters::IssueShow.new(issue).present
else else
Gitlab::ChatCommands::Presenters::Access.new.not_found Gitlab::SlashCommands::Presenters::Access.new.not_found
end end
end end
end end
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class Access < Presenters::Base class Access < Presenters::Base
def access_denied def access_denied
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class Base class Base
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class Deploy < Presenters::Base class Deploy < Presenters::Base
def present(from, to) def present(from, to)
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class Help < Presenters::Base class Help < Presenters::Base
def present(trigger, text) def present(trigger, text)
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
module IssueBase module IssueBase
def color(issuable) def color(issuable)
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class IssueNew < Presenters::Base class IssueNew < Presenters::Base
include Presenters::IssueBase include Presenters::IssueBase
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class IssueSearch < Presenters::Base class IssueSearch < Presenters::Base
include Presenters::IssueBase include Presenters::IssueBase
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
module Presenters module Presenters
class IssueShow < Presenters::Base class IssueShow < Presenters::Base
include Presenters::IssueBase include Presenters::IssueBase
......
module Gitlab module Gitlab
module ChatCommands module SlashCommands
Result = Struct.new(:type, :message) Result = Struct.new(:type, :message)
end end
end end
...@@ -55,7 +55,7 @@ module Gitlab ...@@ -55,7 +55,7 @@ module Gitlab
end end
def service_desk_counts def service_desk_counts
return {} unless ::License.current&.feature_available?(:service_desk) return {} unless ::License.feature_available?(:service_desk)
projects_with_service_desk = Project.where(service_desk_enabled: true) projects_with_service_desk = Project.where(service_desk_enabled: true)
......
...@@ -701,7 +701,7 @@ describe Projects::IssuesController do ...@@ -701,7 +701,7 @@ describe Projects::IssuesController do
end end
end end
context 'when description has slash commands' do context 'when description has quick actions' do
before do before do
sign_in(user) sign_in(user)
end end
......
require 'spec_helper' require 'spec_helper'
describe Projects::ServiceDeskController do describe Projects::ServiceDeskController do
let(:project) { create(:project_empty_repo, :private) } let(:project) { create(:project_empty_repo, :private, service_desk_enabled: true) }
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk) { true } allow(License).to receive(:feature_available?).with(:service_desk) { true }
allow(Gitlab::IncomingEmail).to receive(:enabled?) { true } allow(Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true } allow(Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
project.update(service_desk_enabled: true)
project.add_master(user) project.add_master(user)
sign_in(user) sign_in(user)
end end
...@@ -41,7 +41,7 @@ describe Projects::ServiceDeskController do ...@@ -41,7 +41,7 @@ describe Projects::ServiceDeskController do
describe 'PUT service desk properties' do describe 'PUT service desk properties' do
it 'toggles services desk incoming email' do it 'toggles services desk incoming email' do
project.update(service_desk_enabled: false) project.update!(service_desk_enabled: false)
put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json
......
...@@ -15,9 +15,19 @@ FactoryGirl.define do ...@@ -15,9 +15,19 @@ FactoryGirl.define do
end end
notify_users_at { |l| l.expires_at } notify_users_at { |l| l.expires_at }
notify_admins_at { |l| l.expires_at } notify_admins_at { |l| l.expires_at }
trait :trial do
restrictions do
{ trial: true }
end
end
end end
factory :license do factory :license do
data { build(:gitlab_license).export } data { build(:gitlab_license).export }
end end
factory :trial_license, class: License do
data { build(:gitlab_license, :trial).export }
end
end end
...@@ -51,6 +51,8 @@ describe 'Admin::AuditLogs', feature: true, js: true do ...@@ -51,6 +51,8 @@ describe 'Admin::AuditLogs', feature: true, js: true do
wait_for_requests wait_for_requests
find('.select2-results').click find('.select2-results').click
find('#events-table td', match: :first)
expect(page).to have_content('Added user access as Owner') expect(page).to have_content('Added user access as Owner')
end end
end end
...@@ -73,6 +75,8 @@ describe 'Admin::AuditLogs', feature: true, js: true do ...@@ -73,6 +75,8 @@ describe 'Admin::AuditLogs', feature: true, js: true do
wait_for_requests wait_for_requests
find('.select2-results').click find('.select2-results').click
find('#events-table td', match: :first)
expect(page).to have_content('Removed user access') expect(page).to have_content('Removed user access')
end end
end end
......
...@@ -81,13 +81,13 @@ describe 'Awards Emoji', feature: true do ...@@ -81,13 +81,13 @@ describe 'Awards Emoji', feature: true do
end end
end end
context 'execute /award slash command' do context 'execute /award quick action' do
it 'toggles the emoji award on noteable', js: true do it 'toggles the emoji award on noteable', js: true do
execute_slash_command('/award :100:') execute_quick_action('/award :100:')
expect(find(noteable_award_counter)).to have_text("1") expect(find(noteable_award_counter)).to have_text("1")
execute_slash_command('/award :100:') execute_quick_action('/award :100:')
expect(page).not_to have_selector(noteable_award_counter) expect(page).not_to have_selector(noteable_award_counter)
end end
...@@ -105,7 +105,7 @@ describe 'Awards Emoji', feature: true do ...@@ -105,7 +105,7 @@ describe 'Awards Emoji', feature: true do
end end
end end
def execute_slash_command(cmd) def execute_quick_action(cmd)
within('.js-main-target-form') do within('.js-main-target-form') do
fill_in 'note[note]', with: cmd fill_in 'note[note]', with: cmd
click_button 'Comment' click_button 'Comment'
......
...@@ -208,7 +208,7 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -208,7 +208,7 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).not_to have_selector('.atwho-view') expect(page).not_to have_selector('.atwho-view')
end end
it 'triggers autocomplete after selecting a slash command' do it 'triggers autocomplete after selecting a quick action' do
note = find('#note_note') note = find('#note_note')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('') note.native.send_keys('')
......
require 'rails_helper' require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do feature 'Issues > User uses quick actions', feature: true, js: true do
include SlashCommandsHelpers include QuickActionsHelpers
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
let(:issuable) { create(:issue, project: project) } let(:issuable) { create(:issue, project: project) }
end end
......
require 'rails_helper' require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do feature 'Merge Requests > User uses quick actions', feature: true, js: true do
include SlashCommandsHelpers include QuickActionsHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) } let(:issuable) { create(:merge_request, source_project: project) }
let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
end end
......
...@@ -7,8 +7,8 @@ describe 'Service Desk Setting', js: true, feature: true do ...@@ -7,8 +7,8 @@ describe 'Service Desk Setting', js: true, feature: true do
before do before do
project.add_master(user) project.add_master(user)
login_as(user) login_as(user)
allow_any_instance_of(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk) { true } allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true } allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true } allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
......
...@@ -533,46 +533,46 @@ import '~/notes'; ...@@ -533,46 +533,46 @@ import '~/notes';
}); });
}); });
describe('hasSlashCommands', () => { describe('hasQuickActions', () => {
beforeEach(() => { beforeEach(() => {
this.notes = new Notes('', []); this.notes = new Notes('', []);
}); });
it('should return true when comment begins with a slash command', () => { it('should return true when comment begins with a quick action', () => {
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasSlashCommands).toBeTruthy(); expect(hasQuickActions).toBeTruthy();
}); });
it('should return false when comment does NOT begin with a slash command', () => { it('should return false when comment does NOT begin with a quick action', () => {
const sampleComment = 'Hey, /unassign Merging this'; const sampleComment = 'Hey, /unassign Merging this';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasSlashCommands).toBeFalsy(); expect(hasQuickActions).toBeFalsy();
}); });
it('should return false when comment does NOT have any slash commands', () => { it('should return false when comment does NOT have any quick actions', () => {
const sampleComment = 'Looking good, Awesome!'; const sampleComment = 'Looking good, Awesome!';
const hasSlashCommands = this.notes.hasSlashCommands(sampleComment); const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasSlashCommands).toBeFalsy(); expect(hasQuickActions).toBeFalsy();
}); });
}); });
describe('stripSlashCommands', () => { describe('stripQuickActions', () => {
it('should strip slash commands from the comment which begins with a slash command', () => { it('should strip quick actions from the comment which begins with a quick action', () => {
this.notes = new Notes(); this.notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this'; const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment); const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(''); expect(stripedComment).toBe('');
}); });
it('should strip slash commands from the comment but leaves plain comment if it is present', () => { it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes(); this.notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this'; const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripSlashCommands(sampleComment); const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this'); expect(stripedComment).toBe('Merging this');
}); });
...@@ -580,14 +580,14 @@ import '~/notes'; ...@@ -580,14 +580,14 @@ import '~/notes';
it('should NOT strip string that has slashes within', () => { it('should NOT strip string that has slashes within', () => {
this.notes = new Notes(); this.notes = new Notes();
const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1'; const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripSlashCommands(sampleComment); const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment); expect(stripedComment).toBe(sampleComment);
}); });
}); });
describe('getSlashCommandDescription', () => { describe('getQuickActionDescription', () => {
const availableSlashCommands = [ const availableQuickActions = [
{ name: 'close', description: 'Close this issue', params: [] }, { name: 'close', description: 'Close this issue', params: [] },
{ name: 'title', description: 'Change title', params: [{}] }, { name: 'title', description: 'Change title', params: [{}] },
{ name: 'estimate', description: 'Set time estimate', params: [{}] } { name: 'estimate', description: 'Set time estimate', params: [{}] }
...@@ -597,19 +597,19 @@ import '~/notes'; ...@@ -597,19 +597,19 @@ import '~/notes';
this.notes = new Notes(); this.notes = new Notes();
}); });
it('should return executing slash command description when note has single slash command', () => { it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close'; const sampleComment = '/close';
expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying command to close this issue'); expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying command to close this issue');
}); });
it('should return generic multiple slash command description when note has multiple slash commands', () => { it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar'; const sampleComment = '/close\n/title [Duplicate] Issue foobar';
expect(this.notes.getSlashCommandDescription(sampleComment, availableSlashCommands)).toBe('Applying multiple commands'); expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying multiple commands');
}); });
it('should return generic slash command description when available slash commands list is not populated', () => { it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar'; const sampleComment = '/close\n/title [Duplicate] Issue foobar';
expect(this.notes.getSlashCommandDescription(sampleComment)).toBe('Applying command'); expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
}); });
}); });
......
...@@ -235,6 +235,15 @@ describe EE::Gitlab::LDAP::Sync::Group, lib: true do ...@@ -235,6 +235,15 @@ describe EE::Gitlab::LDAP::Sync::Group, lib: true do
expect(group.members.find_by(user_id: user.id).access_level) expect(group.members.find_by(user_id: user.id).access_level)
.to eq(::Gitlab::Access::OWNER) .to eq(::Gitlab::Access::OWNER)
end end
it 'updates projects authorizations' do
project = create(:empty_project, namespace: group)
group.add_user(user, Gitlab::Access::MASTER)
sync_group.update_permissions
expect(project.authorized_users.find_by(id: user.id)).to be_nil
end
end end
context 'when the user is the last owner' do context 'when the user is the last owner' do
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe EE::Gitlab::ServiceDesk, lib: true do describe EE::Gitlab::ServiceDesk, lib: true do
before do before do
stub_feature(:service_desk) stub_feature(:service_desk, true)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true } allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true } allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end end
...@@ -13,7 +13,7 @@ describe EE::Gitlab::ServiceDesk, lib: true do ...@@ -13,7 +13,7 @@ describe EE::Gitlab::ServiceDesk, lib: true do
context 'when license does not support service desk' do context 'when license does not support service desk' do
before do before do
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk) { false } stub_feature(:service_desk, false)
end end
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
......
...@@ -91,7 +91,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do ...@@ -91,7 +91,7 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
end end
end end
context 'when the note contains slash commands' do context 'when the note contains quick actions' do
let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") } let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
context 'and current user cannot update noteable' do context 'and current user cannot update noteable' do
......
...@@ -10,19 +10,16 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do ...@@ -10,19 +10,16 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
let(:email_raw) { fixture_file('emails/service_desk.eml') } let(:email_raw) { fixture_file('emails/service_desk.eml') }
let(:namespace) { create(:namespace, name: "email") } let(:namespace) { create(:namespace, name: "email") }
let(:project) { create(:project, :public, namespace: namespace, path: "test") }
context 'when service desk is enabled' do context 'service desk is enabled for the project' do
before do let(:project) { create(:empty_project, :public, namespace: namespace, path: 'test', service_desk_enabled: true) }
project.update(service_desk_enabled: true)
before do
allow(Notify).to receive(:service_desk_thank_you_email) allow(Notify).to receive(:service_desk_thank_you_email)
.with(kind_of(Integer)).and_return(double(deliver_later!: true)) .with(kind_of(Integer)).and_return(double(deliver_later!: true))
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(true)
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk) { true } allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
end end
it 'sends thank you the email and creates issue' do it 'sends thank you the email and creates issue' do
...@@ -56,8 +53,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do ...@@ -56,8 +53,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
context 'when license does not support service desk' do context 'when license does not support service desk' do
before do before do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(::EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk) { false }
end end
it 'does not create an issue or send email' do it 'does not create an issue or send email' do
...@@ -88,16 +84,14 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do ...@@ -88,16 +84,14 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
end end
end end
context 'when service desk is not enabled' do context 'service desk is disabled for the project' do
before do let(:project) { create(:empty_project, :public, namespace: namespace, path: 'test') }
project.update_attributes(service_desk_enabled: false)
end
it 'bounces the email' do it 'bounces the email' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProcessingError) expect { receiver.execute }.to raise_error(Gitlab::Email::ProcessingError)
end end
it 'doesn\'t create an issue' do it "doesn't create an issue" do
expect { receiver.execute rescue nil }.not_to change { Issue.count } expect { receiver.execute rescue nil }.not_to change { Issue.count }
end end
end end
......
...@@ -13,15 +13,15 @@ describe Gitlab::Email::Handler, lib: true do ...@@ -13,15 +13,15 @@ describe Gitlab::Email::Handler, lib: true do
context 'a Service Desk email' do context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do it 'uses the Service Desk handler when Service Desk is enabled' do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk).and_return(true) allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler) expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end end
it 'uses no handler when Service Desk is disabled' do it 'uses no handler when Service Desk is disabled' do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk).and_return(false) allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_nil expect(handler_for('emails/service_desk.eml', 'some/project')).to be_nil
end end
...@@ -31,15 +31,15 @@ describe Gitlab::Email::Handler, lib: true do ...@@ -31,15 +31,15 @@ describe Gitlab::Email::Handler, lib: true do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') } let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do it 'uses the create issue handler when Service Desk is enabled' do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk).and_return(true) allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler) expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end end
it 'uses the create issue handler when Service Desk is disabled' do it 'uses the create issue handler when Service Desk is disabled' do
allow_any_instance_of(License).to receive(:feature_available?).and_call_original allow(License).to receive(:feature_available?).and_call_original
allow_any_instance_of(License).to receive(:feature_available?).with(:service_desk).and_return(false) allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler) expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end end
......
...@@ -3,12 +3,13 @@ require 'spec_helper' ...@@ -3,12 +3,13 @@ require 'spec_helper'
describe Gitlab::GitAccess, lib: true do describe Gitlab::GitAccess, lib: true do
let(:pull_access_check) { access.check('git-upload-pack', '_any') } let(:pull_access_check) { access.check('git-upload-pack', '_any') }
let(:push_access_check) { access.check('git-receive-pack', '_any') } let(:push_access_check) { access.check('git-receive-pack', '_any') }
let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities) } let(:access) { Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:actor) { user } let(:actor) { user }
let(:protocol) { 'ssh' } let(:protocol) { 'ssh' }
let(:redirected_path) { nil }
let(:authentication_abilities) do let(:authentication_abilities) do
[ [
:read_project, :read_project,
...@@ -163,6 +164,46 @@ describe Gitlab::GitAccess, lib: true do ...@@ -163,6 +164,46 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
describe '#check_project_moved!' do
before do
project.team << [user, :master]
end
context 'when a redirect was not followed to find the project' do
context 'pull code' do
it { expect { pull_access_check }.not_to raise_error }
end
context 'push code' do
it { expect { push_access_check }.not_to raise_error }
end
end
context 'when a redirect was followed to find the project' do
let(:redirected_path) { 'some/other-path' }
context 'pull code' do
it { expect { pull_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) }
it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) }
context 'http protocol' do
let(:protocol) { 'http' }
it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) }
end
end
context 'push code' do
it { expect { push_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) }
it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) }
context 'http protocol' do
let(:protocol) { 'http' }
it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) }
end
end
end
end
describe '#check_command_disabled!' do describe '#check_command_disabled!' do
before do before do
project.team << [user, :master] project.team << [user, :master]
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do describe Gitlab::GitAccessWiki, lib: true do
let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) } let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let!(:project) { create(:project, :repository) } let!(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
let(:redirected_path) { nil }
let(:authentication_abilities) do let(:authentication_abilities) do
[ [
:read_project, :read_project,
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::SlashCommands::CommandDefinition do describe Gitlab::QuickActions::CommandDefinition do
subject { described_class.new(:command) } subject { described_class.new(:command) }
describe "#all_names" do describe "#all_names" do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::SlashCommands::Dsl do describe Gitlab::QuickActions::Dsl do
before :all do before :all do
DummyClass = Struct.new(:project) do DummyClass = Struct.new(:project) do
include Gitlab::SlashCommands::Dsl # rubocop:disable RSpec/DescribedClass include Gitlab::QuickActions::Dsl # rubocop:disable RSpec/DescribedClass
desc 'A command with no args' desc 'A command with no args'
command :no_args, :none do command :no_args, :none do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::SlashCommands::Extractor do describe Gitlab::QuickActions::Extractor do
let(:definitions) do let(:definitions) do
Class.new do Class.new do
include Gitlab::SlashCommands::Dsl include Gitlab::QuickActions::Dsl
command(:reopen, :open) { } command(:reopen, :open) { }
command(:assign) { } command(:assign) { }
......
...@@ -4,24 +4,44 @@ describe ::Gitlab::RepoPath do ...@@ -4,24 +4,44 @@ describe ::Gitlab::RepoPath do
describe '.parse' do describe '.parse' do
set(:project) { create(:project) } set(:project) { create(:project) }
it 'parses a full repository path' do context 'a repository storage path' do
expect(described_class.parse(project.repository.path)).to eq([project, false]) it 'parses a full repository path' do
end expect(described_class.parse(project.repository.path)).to eq([project, false, nil])
end
it 'parses a full wiki path' do it 'parses a full wiki path' do
expect(described_class.parse(project.wiki.repository.path)).to eq([project, true]) expect(described_class.parse(project.wiki.repository.path)).to eq([project, true, nil])
end
end end
it 'parses a relative repository path' do context 'a relative path' do
expect(described_class.parse(project.full_path + '.git')).to eq([project, false]) it 'parses a relative repository path' do
end expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil])
end
it 'parses a relative wiki path' do it 'parses a relative wiki path' do
expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true]) expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil])
end end
it 'parses a relative path starting with /' do
expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil])
end
context 'of a redirected project' do
let(:redirect) { project.route.create_redirect('foo/bar') }
it 'parses a relative repository path' do
expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar'])
end
it 'parses a relative wiki path' do
expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki'])
end
it 'parses a relative path starting with /' do it 'parses a relative path starting with /' do
expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false]) expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar'])
end
end
end end
end end
...@@ -43,4 +63,33 @@ describe ::Gitlab::RepoPath do ...@@ -43,4 +63,33 @@ describe ::Gitlab::RepoPath do
) )
end end
end end
describe '.find_project' do
let(:project) { create(:empty_project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') }
context 'when finding a project by its canonical path' do
context 'when the cases match' do
it 'returns the project and false' do
expect(described_class.find_project(project.full_path)).to eq([project, false])
end
end
context 'when the cases do not match' do
# This is slightly different than web behavior because on the web it is
# easy and safe to redirect someone to the correctly-cased URL. For git
# requests, we should accept wrongly-cased URLs because it is a pain to
# block people's git operations and force them to update remote URLs.
it 'returns the project and false' do
expect(described_class.find_project(project.full_path.upcase)).to eq([project, false])
end
end
end
context 'when finding a project via a redirect' do
it 'returns the project and true' do
expect(described_class.find_project(redirect.path)).to eq([project, true])
end
end
end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::ChatCommands::Command, service: true do describe Gitlab::SlashCommands::Command, service: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -93,19 +93,19 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -93,19 +93,19 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'IssueShow is triggered' do context 'IssueShow is triggered' do
let(:params) { { text: 'issue show 123' } } let(:params) { { text: 'issue show 123' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) } it { is_expected.to eq(Gitlab::SlashCommands::IssueShow) }
end end
context 'IssueCreate is triggered' do context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } } let(:params) { { text: 'issue create my title' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } it { is_expected.to eq(Gitlab::SlashCommands::IssueNew) }
end end
context 'IssueSearch is triggered' do context 'IssueSearch is triggered' do
let(:params) { { text: 'issue search my query' } } let(:params) { { text: 'issue search my query' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) } it { is_expected.to eq(Gitlab::SlashCommands::IssueSearch) }
end end
end end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::ChatCommands::Deploy, service: true do describe Gitlab::SlashCommands::Deploy, service: true do
describe '#execute' do describe '#execute' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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