Commit acce84d4 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-07-06

parents 85988037 89e685fb
...@@ -7,7 +7,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -7,7 +7,7 @@ Please view this file on the master branch, on stable branches it's out of date.
## 9.3.4 (2017-07-03) ## 9.3.4 (2017-07-03)
- No changes. - Update gitlab-shell to 5.1.1 to fix Post Recieve errors
## 9.3.3 (2017-06-30) ## 9.3.3 (2017-06-30)
......
...@@ -12,7 +12,7 @@ entry. ...@@ -12,7 +12,7 @@ entry.
## 9.3.4 (2017-07-03) ## 9.3.4 (2017-07-03)
- No changes. - Update gitlab-shell to 5.1.1 !12615
## 9.3.3 (2017-06-30) ## 9.3.3 (2017-06-30)
......
...@@ -60,6 +60,7 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -60,6 +60,7 @@ import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels'; import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags'; import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me'; import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -541,6 +542,10 @@ import AuditLogs from './audit_logs'; ...@@ -541,6 +542,10 @@ import AuditLogs from './audit_logs';
if (!shortcut_handler) { if (!shortcut_handler) {
new Shortcuts(); new Shortcuts();
} }
if (document.querySelector('#peek')) {
new PerformanceBar({ container: '#peek' });
}
}; };
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
......
...@@ -15,6 +15,10 @@ class FilteredSearchManager { ...@@ -15,6 +15,10 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container'); this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: this.filteredSearchInput.dataset.multipleAssignees,
});
if (this.page === 'issues' || this.page === 'boards') { if (this.page === 'issues' || this.page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE;
} }
......
...@@ -19,12 +19,18 @@ const weightConditions = [{ ...@@ -19,12 +19,18 @@ const weightConditions = [{
}]; }];
class FilteredSearchTokenKeysIssuesEE extends gl.FilteredSearchTokenKeys { class FilteredSearchTokenKeysIssuesEE extends gl.FilteredSearchTokenKeys {
static init(availableFeatures) {
this.availableFeatures = availableFeatures;
}
static get() { static get() {
const tokenKeys = Array.from(super.get()); const tokenKeys = Array.from(super.get());
// Enable multiple assignees // Enable multiple assignees when available
const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee'); if (this.availableFeatures && this.availableFeatures.multipleAssignees) {
assigneeTokenKey.type = 'array'; const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee');
assigneeTokenKey.type = 'array';
}
tokenKeys.push(weightTokenKey); tokenKeys.push(weightTokenKey);
return tokenKeys; return tokenKeys;
......
import 'vendor/peek';
import 'vendor/peek.performance_bar';
$(document).on('click', '#peek-show-queries', (e) => {
e.preventDefault();
$('.peek-rblineprof-modal').hide();
const $modal = $('#modal-peek-pg-queries');
if ($modal.length) {
$modal.modal('toggle');
}
});
$(document).on('click', '.js-lineprof-file', (e) => {
e.preventDefault();
$(e.target).parents('.peek-rblineprof-file').find('.data').toggle();
});
import 'vendor/peek';
import 'vendor/peek.performance_bar';
export default class PerformanceBar {
constructor(opts) {
if (!PerformanceBar.singleton) {
this.init(opts);
PerformanceBar.singleton = this;
}
return PerformanceBar.singleton;
}
init(opts) {
const $container = $(opts.container);
this.$sqlProfileLink = $container.find('.js-toggle-modal-peek-sql');
this.$sqlProfileModal = $container.find('#modal-peek-pg-queries');
this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
this.$lineProfileModal = $('#modal-peek-line-profile');
this.initEventListeners();
this.showModalOnLoad();
}
initEventListeners() {
this.$sqlProfileLink.on('click', () => this.handleSQLProfileLink());
this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
$(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
}
showModalOnLoad() {
// When a lineprofiler query-string param is present, we show the line
// profiler modal upon page load
if (/lineprofiler/.test(window.location.search)) {
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
handleSQLProfileLink() {
PerformanceBar.toggleModal(this.$sqlProfileModal);
}
handleLineProfileLink(e) {
const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href);
if (shouldToggleModal) {
e.preventDefault();
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
static toggleModal($modal) {
if ($modal.length) {
$modal.modal('toggle');
}
}
static toggleLineProfileFile(e) {
$(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle();
}
}
...@@ -33,12 +33,14 @@ export default { ...@@ -33,12 +33,14 @@ export default {
saveAssignees() { saveAssignees() {
this.loading = true; this.loading = true;
function setLoadingFalse() {
this.loading = false;
}
this.mediator.saveAssignees(this.field) this.mediator.saveAssignees(this.field)
.then(() => { .then(setLoadingFalse.bind(this))
this.loading = false;
})
.catch(() => { .catch(() => {
this.loading = false; setLoadingFalse();
return new Flash('Error occurred when saving assignees'); return new Flash('Error occurred when saving assignees');
}); });
}, },
......
...@@ -21,3 +21,9 @@ body.modal-open { ...@@ -21,3 +21,9 @@ body.modal-open {
width: 860px; width: 860px;
} }
} }
@media (min-width: $screen-lg-min) {
.modal-full {
width: 98%;
}
}
...@@ -614,3 +614,15 @@ Convdev Index ...@@ -614,3 +614,15 @@ Convdev Index
$color-high-score: $green-400; $color-high-score: $green-400;
$color-average-score: $orange-400; $color-average-score: $orange-400;
$color-low-score: $red-400; $color-low-score: $red-400;
/*
Performance Bar
*/
$perf-bar-text: #999;
$perf-bar-production: #222;
$perf-bar-staging: #291430;
$perf-bar-development: #4c1210;
$perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
//= require peek/views/performance_bar @import "framework/variables";
//= require peek/views/rblineprof @import "peek/views/performance_bar";
@import "peek/views/rblineprof";
header.navbar-gitlab.with-peek {
top: 35px;
}
#peek { #peek {
height: 35px; height: 35px;
background: #000; background: $black;
line-height: 35px; line-height: 35px;
color: #999; color: $perf-bar-text;
&.disabled { &.disabled {
display: none; display: none;
} }
&.production { &.production {
background-color: #222; background-color: $perf-bar-production;
} }
&.staging { &.staging {
background-color: #291430; background-color: $perf-bar-staging;
} }
&.development { &.development {
background-color: #4c1210; background-color: $perf-bar-development;
} }
.wrapper { .wrapper {
width: 800px; width: 1000px;
margin: 0 auto; margin: 0 auto;
} }
// UI Elements // UI Elements
.bucket { .bucket {
background: #111; background: $perf-bar-bucket-bg;
display: inline-block; display: inline-block;
padding: 4px 6px; padding: 4px 6px;
font-family: Consolas, "Liberation Mono", Courier, monospace; font-family: Consolas, "Liberation Mono", Courier, monospace;
line-height: 1; line-height: 1;
color: #ccc; color: $perf-bar-bucket-color;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 1px 0 rgba(255,255,255,.2), inset 0 1px 2px rgba(0,0,0,.25); box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
.hidden { .hidden {
display: none; display: none;
...@@ -53,12 +50,14 @@ header.navbar-gitlab.with-peek { ...@@ -53,12 +50,14 @@ header.navbar-gitlab.with-peek {
} }
strong { strong {
color: #fff; color: $white-light;
} }
table { table {
color: $black;
strong { strong {
color: #000; color: $black;
} }
} }
...@@ -90,5 +89,15 @@ header.navbar-gitlab.with-peek { ...@@ -90,5 +89,15 @@ header.navbar-gitlab.with-peek {
} }
#modal-peek-pg-queries-content { #modal-peek-pg-queries-content {
color: #000; color: $black;
}
.peek-rblineprof-file {
pre.duration {
width: 280px;
}
.data {
overflow: visible;
}
} }
...@@ -183,7 +183,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -183,7 +183,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:mirror_max_delay, :mirror_max_delay,
:mirror_max_capacity, :mirror_max_capacity,
:mirror_capacity_threshold, :mirror_capacity_threshold,
:authorized_keys_enabled :authorized_keys_enabled,
:slack_app_enabled,
:slack_app_id,
:slack_app_secret,
:slack_app_verification_token
] ]
end end
end end
module Projects
module Settings
class SlacksController < Projects::ApplicationController
before_action :handle_oauth_error, only: :slack_auth
before_action :authorize_admin_project!
def slack_auth
result = Projects::SlackApplicationInstallService.new(project, current_user, params).execute
if result[:status] == :error
flash[:alert] = result[:message]
end
redirect_to_service_page
end
def destroy
service = project.gitlab_slack_application_service
service.slack_integration.destroy
redirect_to_service_page
end
private
def redirect_to_service_page
redirect_to edit_project_service_path(
project,
project.gitlab_slack_application_service || project.build_gitlab_slack_application_service
)
end
def handle_oauth_error
if params[:error] == 'access_denied'
flash[:alert] = 'Access denied'
redirect_to_service_page
end
end
end
end
end
...@@ -2,6 +2,8 @@ module AuthHelper ...@@ -2,6 +2,8 @@ module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
delegate :slack_app_id, to: :current_application_settings
def ldap_enabled? def ldap_enabled?
Gitlab::LDAP::Config.enabled? Gitlab::LDAP::Config.enabled?
end end
...@@ -72,5 +74,9 @@ module AuthHelper ...@@ -72,5 +74,9 @@ module AuthHelper
%w(saml cas3).exclude?(provider.to_s) %w(saml cas3).exclude?(provider.to_s)
end end
def slack_redirect_uri(project)
slack_auth_project_settings_slack_url(project)
end
extend self extend self
end end
module EE
module FormHelper
def issue_assignees_dropdown_options
options = super
if @project.feature_available?(:multiple_issue_assignees)
options[:title] = 'Select assignee(s)'
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end
end
end
module EE
module SearchHelper
def search_filter_input_options(type)
options = super
options[:data][:'multiple-assignees'] = 'true' if search_multiple_assignees?(type)
options
end
private
def search_multiple_assignees?(type)
type == :issues &&
@project.feature_available?(:multiple_issue_assignees)
end
end
end
module FormHelper module FormHelper
prepend ::EE::FormHelper
def form_errors(model) def form_errors(model)
return unless model.errors.any? return unless model.errors.any?
...@@ -28,7 +30,11 @@ module FormHelper ...@@ -28,7 +30,11 @@ module FormHelper
null_user: true, null_user: true,
current_user: true, current_user: true,
project_id: @project.id, project_id: @project.id,
<<<<<<< HEAD
field_name: 'issue[assignee_ids][]', field_name: 'issue[assignee_ids][]',
=======
field_name: "issue[assignee_ids][]",
>>>>>>> upstream/master
default_label: 'Unassigned', default_label: 'Unassigned',
'max-select': 1, 'max-select': 1,
'dropdown-header': 'Assignee', 'dropdown-header': 'Assignee',
......
...@@ -23,7 +23,6 @@ module NavHelper ...@@ -23,7 +23,6 @@ module NavHelper
def nav_header_class def nav_header_class
class_name = '' class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav class_name << " with-horizontal-nav" if defined?(nav) && nav
class_name << " with-peek" if peek_enabled?
class_name class_name
end end
......
module PerformanceBarHelper
# This is a hack since using `alias_method :performance_bar_enabled?, :peek_enabled?`
# in WithPerformanceBar breaks tests (but works in the browser).
def performance_bar_enabled?
peek_enabled?
end
end
module SearchHelper module SearchHelper
prepend EE::SearchHelper
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
......
...@@ -266,7 +266,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -266,7 +266,11 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'] usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
slack_app_enabled: false,
slack_app_id: nil,
slack_app_secret: nil,
slack_app_verification_token: nil
} }
end end
......
...@@ -109,10 +109,13 @@ module Issuable ...@@ -109,10 +109,13 @@ module Issuable
def allows_multiple_assignees? def allows_multiple_assignees?
false false
end end
<<<<<<< HEAD
def has_multiple_assignees? def has_multiple_assignees?
assignees.count > 1 assignees.count > 1
end end
=======
>>>>>>> upstream/master
end end
module ClassMethods module ClassMethods
......
...@@ -5,6 +5,11 @@ module EE ...@@ -5,6 +5,11 @@ module EE
author.support_bot? || super author.support_bot? || super
end end
# override
def allows_multiple_assignees?
project.feature_available?(:multiple_issue_assignees)
end
# override # override
def subscribed_without_subscriptions?(user, *) def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support # TODO: this really shouldn't be necessary, because the support
......
...@@ -29,6 +29,26 @@ module EE ...@@ -29,6 +29,26 @@ module EE
validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true
end end
def move_dir
raise NotImplementedError unless defined?(super)
succeeded = super
if succeeded
all_projects.each do |project|
old_path_with_namespace = File.join(full_path_was, project.path)
::Geo::RepositoryRenamedEventStore.new(
project,
old_path: project.path,
old_path_with_namespace: old_path_with_namespace
).create
end
end
succeeded
end
# Checks features (i.e. https://about.gitlab.com/products/) availabily # Checks features (i.e. https://about.gitlab.com/products/) availabily
# for a given Namespace plan. This method should consider ancestor groups # for a given Namespace plan. This method should consider ancestor groups
# being licensed. # being licensed.
......
...@@ -429,6 +429,21 @@ module EE ...@@ -429,6 +429,21 @@ module EE
end end
alias_method :merge_requests_ff_only_enabled?, :merge_requests_ff_only_enabled alias_method :merge_requests_ff_only_enabled?, :merge_requests_ff_only_enabled
def rename_repo
raise NotImplementedError unless defined?(super)
super
path_was = previous_changes['path'].first
old_path_with_namespace = File.join(namespace.full_path, path_was)
::Geo::RepositoryRenamedEventStore.new(
self,
old_path: path_was,
old_path_with_namespace: old_path_with_namespace
).create
end
private private
def licensed_feature_available?(feature) def licensed_feature_available?(feature)
......
...@@ -9,5 +9,9 @@ module Geo ...@@ -9,5 +9,9 @@ module Geo
belongs_to :repository_deleted_event, belongs_to :repository_deleted_event,
class_name: 'Geo::RepositoryDeletedEvent', class_name: 'Geo::RepositoryDeletedEvent',
foreign_key: :repository_deleted_event_id foreign_key: :repository_deleted_event_id
belongs_to :repository_renamed_event,
class_name: 'Geo::RepositoryRenamedEvent',
foreign_key: :repository_renamed_event_id
end end
end end
module Geo
class RepositoryRenamedEvent < ActiveRecord::Base
include Geo::Model
belongs_to :project
validates :project, :repository_storage_name, :repository_storage_path,
:old_path_with_namespace, :new_path_with_namespace,
:old_wiki_path_with_namespace, :new_wiki_path_with_namespace,
:old_path, :new_path, presence: true
end
end
...@@ -18,6 +18,7 @@ class License < ActiveRecord::Base ...@@ -18,6 +18,7 @@ class License < ActiveRecord::Base
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
MERGE_REQUEST_SQUASH_FEATURE = 'GitLab_MergeRequestSquash'.freeze MERGE_REQUEST_SQUASH_FEATURE = 'GitLab_MergeRequestSquash'.freeze
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
...@@ -48,6 +49,7 @@ class License < ActiveRecord::Base ...@@ -48,6 +49,7 @@ class License < ActiveRecord::Base
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE, merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE, merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE,
merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE, merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE,
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
push_rules: PUSH_RULES_FEATURE push_rules: PUSH_RULES_FEATURE
}.freeze }.freeze
...@@ -70,6 +72,7 @@ class License < ActiveRecord::Base ...@@ -70,6 +72,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, { MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 }, { MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 } { RELATED_ISSUES_FEATURE => 1 }
].freeze ].freeze
...@@ -113,6 +116,7 @@ class License < ActiveRecord::Base ...@@ -113,6 +116,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 }, { MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 }, { MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ OBJECT_STORAGE_FEATURE => 1 }, { OBJECT_STORAGE_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 }, { PUSH_RULES_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 } { SERVICE_DESK_FEATURE => 1 }
......
...@@ -144,6 +144,7 @@ class Namespace < ActiveRecord::Base ...@@ -144,6 +144,7 @@ class Namespace < ActiveRecord::Base
# So we basically we mute exceptions in next actions # So we basically we mute exceptions in next actions
begin begin
send_update_instructions send_update_instructions
true
rescue rescue
# Returning false does not rollback after_* transaction but gives # Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks # us information about failing some of tasks
......
...@@ -89,6 +89,7 @@ class Project < ActiveRecord::Base ...@@ -89,6 +89,7 @@ class Project < ActiveRecord::Base
# Project services # Project services
has_one :campfire_service has_one :campfire_service
has_one :drone_ci_service has_one :drone_ci_service
has_one :gitlab_slack_application_service
has_one :emails_on_push_service has_one :emails_on_push_service
has_one :pipelines_email_service has_one :pipelines_email_service
has_one :irker_service has_one :irker_service
......
class GitlabSlackApplicationService < Service
default_value_for :category, 'chat'
has_one :slack_integration, foreign_key: :service_id
def self.supported_events
%w()
end
def show_active_box?
false
end
def editable?
false
end
def update_active_status
update(active: !!slack_integration)
end
def can_test?
false
end
def title
'Slack application'
end
def description
'Use the GitLab Slack application for this project'
end
def self.to_param
'gitlab_slack_application'
end
def fields
[]
end
end
...@@ -245,7 +245,6 @@ class Service < ActiveRecord::Base ...@@ -245,7 +245,6 @@ class Service < ActiveRecord::Base
prometheus prometheus
pushover pushover
redmine redmine
slack_slash_commands
slack slack
teamcity teamcity
microsoft_teams microsoft_teams
...@@ -254,6 +253,14 @@ class Service < ActiveRecord::Base ...@@ -254,6 +253,14 @@ class Service < ActiveRecord::Base
service_names += %w[mock_ci mock_deployment mock_monitoring] service_names += %w[mock_ci mock_deployment mock_monitoring]
end end
if show_gitlab_slack_application?
service_names.push('gitlab_slack_application')
end
unless Gitlab.com?
service_names.push('slack_slash_commands')
end
service_names.sort_by(&:downcase) service_names.sort_by(&:downcase)
end end
...@@ -264,6 +271,10 @@ class Service < ActiveRecord::Base ...@@ -264,6 +271,10 @@ class Service < ActiveRecord::Base
service service
end end
def self.show_gitlab_slack_application?
(Gitlab.com? && current_application_settings.slack_app_enabled) || Rails.env.development?
end
private private
def cache_project_has_external_issue_tracker def cache_project_has_external_issue_tracker
......
class SlackIntegration < ActiveRecord::Base
belongs_to :service
validates :team_id, presence: true
validates :team_name, presence: true
validates :alias, presence: true,
uniqueness: { scope: :team_id, message: 'This alias has already been taken' }
validates :user_id, presence: true
validates :service, presence: true
after_commit :update_active_status_of_service, on: [:create, :destroy]
def update_active_status_of_service
service.update_active_status
end
end
module EE
module Projects
module TransferService
private
def execute_system_hooks
raise NotImplementedError unless defined?(super)
super
::Geo::RepositoryRenamedEventStore.new(
project,
old_path: project.path,
old_path_with_namespace: @old_path
).create
end
end
end
end
module EE
module QuickActions
module InterpretService
include ::Gitlab::QuickActions::Dsl
desc 'Change assignee(s)'
explanation do
'Change assignee(s)'
end
params '@user1 @user2'
condition do
issuable.allows_multiple_assignees? &&
issuable.persisted? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :reassign do |unassign_param|
@updates[:assignee_ids] = extract_users(unassign_param).map(&:id)
end
desc 'Set weight'
explanation do |weight|
"Sets weight to #{weight}." if weight
end
params ::Issue::WEIGHT_RANGE.to_s.squeeze('.').tr('.', '-')
condition do
issuable.supports_weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |weight|
weight.to_i if ::Issue.weight_filter_options.include?(weight.to_i)
end
command :weight do |weight|
@updates[:weight] = weight if weight
end
desc 'Clear weight'
explanation 'Clears weight.'
condition do
issuable.persisted? &&
issuable.supports_weight? &&
issuable.weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :clear_weight do
@updates[:weight] = nil
end
end
end
end
module Geo
# Base class for event store classes.
#
# Each store should also specify its event type by calling
# `self.event_type = ...` in the body of the class. The value of
# this method should be a symbol such as `:repository_updated_event`
# or `:repository_deleted_event`. For example:
#
# class RepositoryUpdatedEventStore < EventStore
# self.event_type = :repository_updated_event
# end
#
# The event type is used to determine which attribute we should set
# on an instance of the Geo::EventLog class.
#
# Event store classes should implement the instance method `build_event`.
# The `build_event` method is supposed to return an instance of the event
# that will be logged.
class EventStore
class << self
attr_accessor :event_type
end
attr_reader :project, :params
def initialize(project, params = {})
@project = project
@params = params
end
def create
return unless Gitlab::Geo.primary?
Geo::EventLog.create!("#{self.class.event_type}" => build_event)
rescue ActiveRecord::RecordInvalid, NoMethodError => e
log("#{self.event_type.to_s.humanize} could not be created", e)
end
private
def build_event
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
def log(message, error)
Rails.logger.error("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id}): #{error}")
end
end
end
module Geo module Geo
class RepositoryDeletedEventStore class RepositoryDeletedEventStore < EventStore
attr_reader :project, :repo_path, :wiki_path self.event_type = :repository_deleted_event
def initialize(project, repo_path:, wiki_path:) private
@project = project
@repo_path = repo_path
@wiki_path = wiki_path
end
def create
return unless Gitlab::Geo.primary?
Geo::EventLog.transaction do def build_event
event_log = Geo::EventLog.new Geo::RepositoryDeletedEvent.new(
deleted_event = Geo::RepositoryDeletedEvent.new( project: project,
project: project, repository_storage_name: project.repository.storage,
repository_storage_name: project.repository.storage, repository_storage_path: project.repository_storage_path,
repository_storage_path: project.repository_storage_path, deleted_path: params.fetch(:repo_path),
deleted_path: repo_path, deleted_wiki_path: params.fetch(:wiki_path),
deleted_wiki_path: wiki_path, deleted_project_name: project.name)
deleted_project_name: project.name)
event_log.repository_deleted_event = deleted_event
event_log.save
end
end end
end end
end end
module Geo
class RepositoryRenamedEventStore < EventStore
self.event_type = :repository_renamed_event
private
def build_event
Geo::RepositoryRenamedEvent.new(
project: project,
repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path,
old_path_with_namespace: old_path_with_namespace,
new_path_with_namespace: project.full_path,
old_wiki_path_with_namespace: old_wiki_path_with_namespace,
new_wiki_path_with_namespace: new_wiki_path_with_namespace,
old_path: params.fetch(:old_path),
new_path: project.path
)
end
def old_path_with_namespace
params.fetch(:old_path_with_namespace)
end
def old_wiki_path_with_namespace
"#{old_path_with_namespace}.wiki"
end
def new_wiki_path_with_namespace
project.wiki.path_with_namespace
end
end
end
module Geo module Geo
class RepositoryUpdatedEventStore class RepositoryUpdatedEventStore < EventStore
attr_reader :project, :source, :refs, :changes self.event_type = :repository_updated_event
def initialize(project, refs: [], changes: [], source: Geo::RepositoryUpdatedEvent::REPOSITORY)
@project = project
@refs = refs
@changes = changes
@source = source
end
def create
return unless Gitlab::Geo.primary?
Geo::EventLog.transaction do
event_log = Geo::EventLog.new
event_log.repository_updated_event = build_event
event_log.save!
end
rescue ActiveRecord::RecordInvalid
log("#{Geo::PushEvent.sources.key(source).humanize} updated event could not be created")
end
private private
...@@ -35,6 +16,18 @@ module Geo ...@@ -35,6 +16,18 @@ module Geo
) )
end end
def refs
params.fetch(:refs, [])
end
def changes
params.fetch(:changes, [])
end
def source
params.fetch(:source, Geo::RepositoryUpdatedEvent::REPOSITORY)
end
def ref def ref
refs.first if refs.length == 1 refs.first if refs.length == 1
end end
...@@ -54,9 +47,5 @@ module Geo ...@@ -54,9 +47,5 @@ module Geo
def push_remove_branch? def push_remove_branch?
changes.any? { |change| Gitlab::Git.branch_ref?(change[:ref]) && Gitlab::Git.blank_ref?(change[:after]) } changes.any? { |change| Gitlab::Git.branch_ref?(change[:ref]) && Gitlab::Git.blank_ref?(change[:after]) }
end end
def log(message)
Rails.logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end end
end end
...@@ -24,6 +24,10 @@ module Issues ...@@ -24,6 +24,10 @@ module Issues
def filter_assignee(issuable) def filter_assignee(issuable)
return if params[:assignee_ids].blank? return if params[:assignee_ids].blank?
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].take(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) } assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE] if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
......
module Projects
class SlackApplicationInstallService < BaseService
include Gitlab::Routing
SLACK_EXCHANGE_TOKEN_URL = 'https://slack.com/api/oauth.access'.freeze
def execute
slack_data = exchange_slack_token
return error("Slack: #{slack_data['error']}") unless slack_data['ok']
unless project.gitlab_slack_application_service
project.create_gitlab_slack_application_service
end
service = project.gitlab_slack_application_service
SlackIntegration.create!(
service_id: service.id,
team_id: slack_data['team_id'],
team_name: slack_data['team_name'],
alias: project.path_with_namespace,
user_id: slack_data['user_id']
)
make_sure_chat_name_created(slack_data)
success
end
private
def make_sure_chat_name_created(slack_data)
service = project.gitlab_slack_application_service
chat_name = ChatName.find_by(
service: service.id,
team_id: slack_data['team_id'],
chat_id: slack_data['user_id']
)
unless chat_name
ChatName.find_or_create_by!(
service_id: service.id,
team_id: slack_data['team_id'],
team_domain: slack_data['team_name'],
chat_id: slack_data['user_id'],
chat_name: slack_data['user_name'],
user: current_user
)
end
end
def exchange_slack_token
HTTParty.get(SLACK_EXCHANGE_TOKEN_URL, query: {
client_id: current_application_settings.slack_app_id,
client_secret: current_application_settings.slack_app_secret,
redirect_uri: slack_auth_project_settings_slack_url(project),
code: params[:code]
})
end
end
end
...@@ -11,6 +11,8 @@ module Projects ...@@ -11,6 +11,8 @@ module Projects
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
TransferError = Class.new(StandardError) TransferError = Class.new(StandardError)
prepend ::EE::Projects::TransferService
def execute(new_namespace) def execute(new_namespace)
@new_namespace = new_namespace @new_namespace = new_namespace
......
module QuickActions module QuickActions
class InterpretService < BaseService class InterpretService < BaseService
include Gitlab::QuickActions::Dsl include Gitlab::QuickActions::Dsl
prepend EE::QuickActions::InterpretService
attr_reader :issuable attr_reader :issuable
...@@ -136,6 +137,7 @@ module QuickActions ...@@ -136,6 +137,7 @@ module QuickActions
parse_params do |unassign_param| parse_params do |unassign_param|
# When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed
extract_users(unassign_param) if issuable.allows_multiple_assignees? extract_users(unassign_param) if issuable.allows_multiple_assignees?
<<<<<<< HEAD
end end
command :unassign do |users = nil| command :unassign do |users = nil|
@updates[:assignee_ids] = @updates[:assignee_ids] =
...@@ -170,21 +172,16 @@ module QuickActions ...@@ -170,21 +172,16 @@ module QuickActions
else else
[users.last.id] [users.last.id]
end end
=======
>>>>>>> upstream/master
end end
command :unassign do |users = nil|
desc 'Change assignee(s)' @updates[:assignee_ids] =
explanation do if users&.any?
'Change assignee(s)' issuable.assignees.pluck(:id) - users.map(&:id)
end else
params '@user1 @user2' []
condition do end
issuable.is_a?(Issue) &&
issuable.persisted? &&
issuable.assignees.any? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :reassign do |unassign_param|
@updates[:assignee_ids] = extract_users(unassign_param).map(&:id)
end end
desc 'Set milestone' desc 'Set milestone'
...@@ -486,34 +483,6 @@ module QuickActions ...@@ -486,34 +483,6 @@ module QuickActions
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
end end
desc 'Set weight'
explanation do |weight|
"Sets weight to #{weight}." if weight
end
params Issue::WEIGHT_RANGE.to_s.squeeze('.').tr('.', '-')
condition do
issuable.supports_weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |weight|
weight.to_i if Issue.weight_filter_options.include?(weight.to_i)
end
command :weight do |weight|
@updates[:weight] = weight if weight
end
desc 'Clear weight'
explanation 'Clears weight.'
condition do
issuable.persisted? &&
issuable.supports_weight? &&
issuable.weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
command :clear_weight do
@updates[:weight] = nil
end
desc 'Move issue from one column of the board to another' desc 'Move issue from one column of the board to another'
explanation do |target_list_name| explanation do |target_list_name|
label = find_label_references(target_list_name).first label = find_label_references(target_list_name).first
......
module SlashCommands
class GlobalSlackHandler
attr_reader :project_alias, :params
def initialize(params)
@project_alias, command = parse_command_text(params)
@params = params.merge(text: command, original_command: params[:text])
end
def trigger
return false unless valid_token?
if help_command?
return Gitlab::SlashCommands::ApplicationHelp.new(params).execute
end
unless integration = find_integration
error_message = 'GitLab error: project or alias not found'
return Gitlab::SlashCommands::Presenters::Error.new(error_message).message
end
service = integration.service
project = service.project
user = ChatNames::FindUserService.new(service, params).execute
if user
Gitlab::SlashCommands::Command.new(project, user, params).execute
else
url = ChatNames::AuthorizeUserService.new(service, params).execute
Gitlab::SlashCommands::Presenters::Access.new(url).authorize
end
end
private
def valid_token?
ActiveSupport::SecurityUtils.variable_size_secure_compare(
current_application_settings.slack_app_verification_token,
params[:token]
)
end
def help_command?
params[:original_command] == 'help'
end
def find_integration
SlackIntegration.find_by(team_id: params[:team_id], alias: project_alias)
end
# Splits the command
# '/gitlab help' => [nil, 'help']
# '/gitlab group/project issue new some title' => ['group/project', 'issue new some title']
def parse_command_text(params)
fragments = params[:text].split(/\s/, 2)
fragments.size == 1 ? [nil, fragments.first] : fragments
end
end
end
...@@ -688,6 +688,29 @@ ...@@ -688,6 +688,29 @@
if you have configured your OpenSSH server to use the if you have configured your OpenSSH server to use the
AuthorizedKeysCommand. Click on the help icon for more details. AuthorizedKeysCommand. Click on the help icon for more details.
= link_to icon('question-circle'), help_page_path('administration/operations/speed_up_ssh', anchor: 'the-solution') = link_to icon('question-circle'), help_page_path('administration/operations/speed_up_ssh', anchor: 'the-solution')
- if Gitlab.com? || Rails.env.development?
%fieldset
%legend Slack application
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :slack_app_enabled do
= f.check_box :slack_app_enabled
Enable Slack application
.help-block
This option is only available on GitLab.com
.form-group
= f.label :slack_app_id, 'APP_ID', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :slack_app_id, class: 'form-control'
.form-group
= f.label :slack_app_secret, 'APP_SECRET', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :slack_app_secret, class: 'form-control'
.form-group
= f.label :slack_app_verification_token, 'Verification token', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :slack_app_verification_token, class: 'form-control'
- if Gitlab::Geo.license_allows? - if Gitlab::Geo.license_allows?
%fieldset %fieldset
......
...@@ -27,10 +27,11 @@ ...@@ -27,10 +27,11 @@
%td.shortcut %td.shortcut
.key f .key f
%td Focus Filter %td Focus Filter
%tr - if performance_bar_enabled?
%td.shortcut %tr
.key p b %td.shortcut
%td Show/hide the Performance Bar .key p b
%td Show/hide the Performance Bar
%tr %tr
%td.shortcut %td.shortcut
.key ? .key ?
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
= stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'peek' if peek_enabled? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- if show_new_nav? - if show_new_nav?
= stylesheet_link_tag "new_nav", media: "all" = stylesheet_link_tag "new_nav", media: "all"
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test? = webpack_bundle_tag "test" if Rails.env.test?
= webpack_bundle_tag 'peek' if peek_enabled? = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
- if content_for?(:page_specific_javascripts) - if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts = yield :page_specific_javascripts
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
= render "layouts/head" = render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= render "layouts/init_auto_complete" if @gfm_form = render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- if show_new_nav? - if show_new_nav?
= render "layouts/header/new" = render "layouts/header/new"
- else - else
...@@ -11,3 +10,5 @@ ...@@ -11,3 +10,5 @@
= render 'layouts/page', sidebar: sidebar, nav: nav = render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body = yield :scripts_body
= render 'peek/bar'
Profile:
= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile'
\/
= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile'
\/
= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile'
%strong %strong
%a#peek-show-queries{ href: '#' } %a.js-toggle-modal-peek-sql
%span{ data: { defer_to: "#{view.defer_key}-duration" } }... %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/ \/
%span{ data: { defer_to: "#{view.defer_key}-calls" } }... %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 } #modal-peek-pg-queries.modal{ tabindex: -1 }
.modal-dialog .modal-dialog.modal-full
#modal-peek-pg-queries-content.modal-content .modal-content
.modal-header .modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } × %button.close.btn.btn-link.btn-sm{ type: 'button', data: { dismiss: 'modal' } } X
%h4 %h4
SQL queries SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
...@@ -19,10 +19,11 @@ ...@@ -19,10 +19,11 @@
":data-name" => "assignee.name", ":data-name" => "assignee.name",
":data-username" => "assignee.username" } ":data-username" => "assignee.username" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", dropdown: { header: 'Assignee(s)'} }, - dropdown_options = issue_assignees_dropdown_options
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
Select assignee(s) = dropdown_options[:title]
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to") = dropdown_title("Assign to")
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
- disabled_class = 'disabled' - disabled_class = 'disabled'
- disabled_title = @service.disabled_title - disabled_title = @service.disabled_title
= link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr %hr
......
- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/gitlab_slack_application.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
See the list of available commands in Slack after setting up this service
by entering
%kbd.inline /gitlab help
- unless @service.template?
%p To set up this service press "Add to Slack"
= render "projects/services/#{@service.to_param}/slack_integration_form"
%a{ href: "https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_redirect_uri(@project)}" }
%img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" }
= render "projects/services/#{@service.to_param}/slack_button"
- slack_integration = @service.slack_integration
- if slack_integration
%table.table
%colgroup
%col
%col
%col.hidden-xs
%col{ width: "120" }
%thead
%tr
%th Team name
%th Project alias
%th Created at
%th Actions
%tr
%td
= slack_integration.team_name
%td
= slack_integration.alias
%td.light
= time_ago_in_words slack_integration.created_at
ago
%td.light
- project = @service.project
= link_to 'Remove', namespace_project_settings_slack_path(project.namespace, project), method: :delete, class: 'btn btn-danger', data: { confirm: 'Are you sure?' }
...@@ -50,7 +50,11 @@ ...@@ -50,7 +50,11 @@
- data[:multi_select] = true - data[:multi_select] = true
- data['dropdown-title'] = title - data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
<<<<<<< HEAD
- data['max-select'] = dropdown_options[:data][:'max-select'] - data['max-select'] = dropdown_options[:data][:'max-select']
=======
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
>>>>>>> upstream/master
- options[:data].merge!(data) - options[:data].merge!(data)
= dropdown_tag(title, options: options) = dropdown_tag(title, options: options)
---
title: Add Geo repository renamed event log
merge_request:
author:
---
title: "[GitLab.com only] Add Slack applicationq service"
merge_request:
author:
...@@ -111,7 +111,7 @@ module Gitlab ...@@ -111,7 +111,7 @@ module Gitlab
config.assets.precompile << "katex.css" config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js" config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "peek.css" config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js" config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css" config.assets.precompile << "test.css"
......
...@@ -438,6 +438,11 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -438,6 +438,11 @@ constraints(ProjectUrlConstrainer.new) do
resource :members, only: [:show] resource :members, only: [:show]
resource :ci_cd, only: [:show], controller: 'ci_cd' resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show] resource :integrations, only: [:show]
resource :slack, only: [:destroy] do
get :slack_auth
end
resource :repository, only: [:show], controller: :repository resource :repository, only: [:show], controller: :repository
end end
......
...@@ -74,7 +74,7 @@ var config = { ...@@ -74,7 +74,7 @@ var config = {
raven: './raven/index.js', raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
peek: './peek.js', performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, },
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSlackIntegrationtable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
create_table :slack_integrations do |t|
t.belongs_to :service, null: false, foreign_key: { on_delete: :cascade }, index: true
t.string :team_id, null: false
t.string :team_name, null: false
t.string :alias, null: false
t.string :user_id, null: false
t.index [:team_id, :alias], unique: true
t.timestamps_with_timezone
end
end
end
class CreateGeoRepositoryRenamedEvents < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :geo_repository_renamed_events, id: :bigserial do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
t.text :repository_storage_name, null: false
t.text :repository_storage_path, null: false
t.text :old_path_with_namespace, null: false
t.text :new_path_with_namespace, null: false
t.text :old_wiki_path_with_namespace, null: false
t.text :new_wiki_path_with_namespace, null: false
t.text :old_path, null: false
t.text :new_path, null: false
end
add_column :geo_event_log, :repository_renamed_event_id, :integer, limit: 8
end
end
class AddGeoRepositoryRenamedEventsForeignKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_foreign_key :geo_event_log, :geo_repository_renamed_events,
column: :repository_renamed_event_id, on_delete: :cascade
end
def down
remove_foreign_key :geo_event_log, column: :repository_renamed_event_id
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSlackToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :application_settings, :slack_app_enabled, :boolean, default: false
add_column :application_settings, :slack_app_id, :string
add_column :application_settings, :slack_app_secret, :string
add_column :application_settings, :slack_app_verification_token, :string
end
end
...@@ -144,6 +144,10 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -144,6 +144,10 @@ ActiveRecord::Schema.define(version: 20170703102400) do
t.boolean "authorized_keys_enabled", default: true, null: false t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url" t.string "help_page_support_url"
t.boolean "slack_app_enabled", default: false
t.string "slack_app_id"
t.string "slack_app_secret"
t.string "slack_app_verification_token"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -584,6 +588,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -584,6 +588,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.integer "repository_updated_event_id", limit: 8 t.integer "repository_updated_event_id", limit: 8
t.integer "repository_deleted_event_id", limit: 8 t.integer "repository_deleted_event_id", limit: 8
t.integer "repository_renamed_event_id", limit: 8
end end
add_index "geo_event_log", ["repository_updated_event_id"], name: "index_geo_event_log_on_repository_updated_event_id", using: :btree add_index "geo_event_log", ["repository_updated_event_id"], name: "index_geo_event_log_on_repository_updated_event_id", using: :btree
...@@ -621,6 +626,20 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -621,6 +626,20 @@ ActiveRecord::Schema.define(version: 20170703102400) do
add_index "geo_repository_deleted_events", ["project_id"], name: "index_geo_repository_deleted_events_on_project_id", using: :btree add_index "geo_repository_deleted_events", ["project_id"], name: "index_geo_repository_deleted_events_on_project_id", using: :btree
create_table "geo_repository_renamed_events", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.text "repository_storage_name", null: false
t.text "repository_storage_path", null: false
t.text "old_path_with_namespace", null: false
t.text "new_path_with_namespace", null: false
t.text "old_wiki_path_with_namespace", null: false
t.text "new_wiki_path_with_namespace", null: false
t.text "old_path", null: false
t.text "new_path", null: false
end
add_index "geo_repository_renamed_events", ["project_id"], name: "index_geo_repository_renamed_events_on_project_id", using: :btree
create_table "geo_repository_updated_events", id: :bigserial, force: :cascade do |t| create_table "geo_repository_updated_events", id: :bigserial, force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.integer "branches_affected", null: false t.integer "branches_affected", null: false
...@@ -1513,6 +1532,19 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -1513,6 +1532,19 @@ ActiveRecord::Schema.define(version: 20170703102400) do
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree add_index "services", ["template"], name: "index_services_on_template", using: :btree
create_table "slack_integrations", force: :cascade do |t|
t.integer "service_id", null: false
t.string "team_id", null: false
t.string "team_name", null: false
t.string "alias", null: false
t.string "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "slack_integrations", ["team_id", "alias"], name: "index_slack_integrations_on_team_id_and_alias", unique: true, using: :btree
add_index "slack_integrations", ["service_id"], name: "index_slack_integrations_on_service_id", using: :btree
create_table "snippets", force: :cascade do |t| create_table "snippets", force: :cascade do |t|
t.string "title" t.string "title"
t.text "content" t.text "content"
...@@ -1846,7 +1878,9 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -1846,7 +1878,9 @@ ActiveRecord::Schema.define(version: 20170703102400) do
add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade add_foreign_key "events", "projects", name: "fk_0434b48643", on_delete: :cascade
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_deleted_events", column: "repository_deleted_event_id", name: "fk_c4b1c1f66e", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_renamed_events", column: "repository_renamed_event_id", name: "fk_86c84214ec", on_delete: :cascade
add_foreign_key "geo_event_log", "geo_repository_updated_events", column: "repository_updated_event_id", on_delete: :cascade add_foreign_key "geo_event_log", "geo_repository_updated_events", column: "repository_updated_event_id", on_delete: :cascade
add_foreign_key "geo_repository_renamed_events", "projects", on_delete: :cascade
add_foreign_key "geo_repository_updated_events", "projects", on_delete: :cascade add_foreign_key "geo_repository_updated_events", "projects", on_delete: :cascade
add_foreign_key "index_statuses", "projects", name: "fk_74b2492545", on_delete: :cascade add_foreign_key "index_statuses", "projects", name: "fk_74b2492545", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
...@@ -1899,6 +1933,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do ...@@ -1899,6 +1933,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do
add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade add_foreign_key "remote_mirrors", "projects", name: "fk_43a9aa4ca8", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
......
...@@ -31,6 +31,11 @@ connect to `secondary` database servers (which are read-only too). ...@@ -31,6 +31,11 @@ connect to `secondary` database servers (which are read-only too).
In many databases documentation you will see `primary` being references as `master` In many databases documentation you will see `primary` being references as `master`
and `secondary` as either `slave` or `standby` server (read-only). and `secondary` as either `slave` or `standby` server (read-only).
New for GitLab 9.4: We recommend using [PostgreSQL replication
slots](https://medium.com/@tk512/replication-slots-in-postgresql-b4b03d277c75)
to ensure the primary retains all the data necessary for the secondaries to
recover. See below for more details.
### Prerequisites ### Prerequisites
The following guide assumes that: The following guide assumes that:
...@@ -73,6 +78,8 @@ The following guide assumes that: ...@@ -73,6 +78,8 @@ The following guide assumes that:
postgresql['listen_address'] = "1.2.3.4" postgresql['listen_address'] = "1.2.3.4"
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32'] postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32']
postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32'] postgresql['md5_auth_cidr_addresses'] = ['5.6.7.8/32']
# New for 9.4: Set this to be the number of Geo secondary nodes you have
postgresql['max_replication_slots'] = 1
# postgresql['max_wal_senders'] = 10 # postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10 # postgresql['wal_keep_segments'] = 10
``` ```
...@@ -118,6 +125,7 @@ The following guide assumes that: ...@@ -118,6 +125,7 @@ The following guide assumes that:
postgresql['listen_address'] = "10.1.5.3" postgresql['listen_address'] = "10.1.5.3"
postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','10.1.5.3/32'] postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','10.1.5.3/32']
postgresql['md5_auth_cidr_addresses'] = ['10.1.10.5/32'] postgresql['md5_auth_cidr_addresses'] = ['10.1.10.5/32']
postgresql['max_replication_slots'] = 1 # Number of Geo secondary nodes
# postgresql['max_wal_senders'] = 10 # postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10 # postgresql['wal_keep_segments'] = 10
``` ```
...@@ -138,6 +146,8 @@ The following guide assumes that: ...@@ -138,6 +146,8 @@ The following guide assumes that:
1. Check to make sure your firewall rules are set so that the secondary nodes 1. Check to make sure your firewall rules are set so that the secondary nodes
can access port 5432 on the primary node. can access port 5432 on the primary node.
1. Save the file and [reconfigure GitLab][] for the changes to take effect. 1. Save the file and [reconfigure GitLab][] for the changes to take effect.
1. New for 9.4: Restart your primary PostgreSQL server to ensure the replication slot changes
take effect (`sudo gitlab-ctl restart postgresql` for Omnibus-provided PostgreSQL).
1. Now that the PostgreSQL server is set up to accept remote connections, run 1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt` to make sure that PostgreSQL is listening to the server's `netstat -plnt` to make sure that PostgreSQL is listening to the server's
public IP. public IP.
...@@ -196,16 +206,25 @@ data before running `pg_basebackup`. ...@@ -196,16 +206,25 @@ data before running `pg_basebackup`.
sudo -i sudo -i
``` ```
1. New for 9.4: Choose a database-friendly name to use for your secondary to use as the
replication slot name. For example, if your domain is
`geo-secondary.mydomain.com`, you may use `geo_secondary_my_domain_com` as
the slot name.
1. Execute the command below to start a backup/restore and begin the replication: 1. Execute the command below to start a backup/restore and begin the replication:
``` ```
gitlab-ctl replicate-geo-database --host=1.2.3.4 gitlab-ctl replicate-geo-database --host=1.2.3.4 --slot-name=geo-secondary_my_domain_com
``` ```
Change the `--host=` to the primary node IP or FQDN. You can check other possible Change the `--host=` to the primary node IP or FQDN. You can check other possible
parameters with `--help`. When prompted, enter the password you set up for parameters with `--help`. When prompted, enter the password you set up for
the `gitlab_replicator` user in the first step. the `gitlab_replicator` user in the first step.
New for 9.4: Change the `--slot-name` to the name of the replication slot
to be used on the primary database. The script will attempt to create the
replication slot automatically if it does not exist.
The replication process is now over. The replication process is now over.
### Next steps ### Next steps
......
...@@ -68,10 +68,14 @@ The following guide assumes that: ...@@ -68,10 +68,14 @@ The following guide assumes that:
max_wal_senders = 5 max_wal_senders = 5
min_wal_size = 80MB min_wal_size = 80MB
max_wal_size = 1GB max_wal_size = 1GB
max_replicaton_slots = 1 # Number of Geo secondary nodes
wal_keep_segments = 10 wal_keep_segments = 10
hot_standby = on hot_standby = on
``` ```
Be sure to set `max_replication_slots` to the number of Geo secondary
nodes that you may potentially have (at least 1).
See the Omnibus notes above for more details of `listen_address`. See the Omnibus notes above for more details of `listen_address`.
You may also want to edit the `wal_keep_segments` and `max_wal_senders` to You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
...@@ -102,6 +106,22 @@ The following guide assumes that: ...@@ -102,6 +106,22 @@ The following guide assumes that:
``` ```
1. Restart PostgreSQL for the changes to take effect. 1. Restart PostgreSQL for the changes to take effect.
1. Choose a database-friendly name to use for your secondary to use as the
replication slot name. For example, if your domain is
`geo-secondary.mydomain.com`, you may use `geo_secondary_my_domain_com` as
the slot name.
1. Create the replication slot on the primary:
```
$ sudo -u postgres psql -c "SELECT * FROM pg_create_physical_replication_slot('geo_secondary_my_domain');"
slot_name | xlog_position
-------------------------+---------------
geo_secondary_my_domain |
(1 row)
```
1. Now that the PostgreSQL server is set up to accept remote connections, run 1. Now that the PostgreSQL server is set up to accept remote connections, run
`netstat -plnt` to make sure that PostgreSQL is listening to the server's `netstat -plnt` to make sure that PostgreSQL is listening to the server's
public IP. public IP.
......
...@@ -50,6 +50,21 @@ where you have to fix (all commands and path locations are for Omnibus installs) ...@@ -50,6 +50,21 @@ where you have to fix (all commands and path locations are for Omnibus installs)
# remove old entries to your primary gitlab in known_hosts # remove old entries to your primary gitlab in known_hosts
ssh-keyscan -R your-primary-gitlab.example.com ssh-keyscan -R your-primary-gitlab.example.com
- How do I fix the message, "ERROR: replication slots can only be used if max_replication_slots > 0"?
- This means that the `max_replication_slots` PostgreSQL variable needs to
be set on the primary database. In GitLab 9.4, we have made this setting
default to 1. You may need to increase this value if you have more Geo
secondary nodes. Be sure to restart PostgreSQL for this to take
effect. See the [PostgreSQL replication
setup](database.md#postgresql-replication) guide for more details.
- How do I fix the message, "FATAL: could not start WAL streaming: ERROR: replication slot "geo_secondary_my_domain_com" does not exist"?
- This occurs when PostgreSQL does not have a replication slot for the
secondary by that name. You may want to rerun the [replication
process](database.md) on the secondary.
Visit the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in Visit the primary node's **Admin Area ➔ Geo Nodes** (`/admin/geo_nodes`) in
your browser. We perform the following health checks on each secondary node your browser. We perform the following health checks on each secondary node
to help identify if something is wrong: to help identify if something is wrong:
......
# Slack application (only available on GitLab.com)
Since GitLab 9.4 you can install GitLab.com Slack application to get [slash commands](https://docs.gitlab.com/ce/integration/slash_commands.html) working.
The only difference is that all the commands should be prefixed with `/gitlab` keyword:
```
# Show the issue #1001
/gitlab gitlab-org/gitlab-ce issue show 1001
```
To install GitLab application to your Slack team you need to go to
`Project Settings > Integration > Slack application` page and press "Add to Slack" button.
Keep in mind that you have to have appropriate permissions for that team to be able to
install a new application, see details in [Add an app to your team](https://get.slack.help/hc/en-us/articles/202035138-Adding-apps-to-your-team).
After confirming installation you, and everyone else in your Slack team, can use all the commands.
When you perform your first slash command you will be asked to authorize your Slack user
inside GitLab.com.
...@@ -15,7 +15,7 @@ end ...@@ -15,7 +15,7 @@ end
end end
# EE-only # EE-only
%w(license).each do |f| %w(test_license).each do |f|
require Rails.root.join('spec', 'support', f) require Rails.root.join('spec', 'support', f)
end end
......
...@@ -770,5 +770,17 @@ module API ...@@ -770,5 +770,17 @@ module API
end end
end end
end end
desc "Trigger a global slack command" do
detail 'Added in GitLab 9.4'
end
post 'slack/trigger' do
if result = SlashCommands::GlobalSlackHandler.new(params).trigger
status result[:status] || 200
present result
else
not_found!
end
end
end end
end end
...@@ -82,7 +82,7 @@ module Gitlab ...@@ -82,7 +82,7 @@ module Gitlab
end end
def handle_repository_update(updated_event) def handle_repository_update(updated_event)
registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: project_id) registry = ::Geo::ProjectRegistry.find_or_initialize_by(project_id: updated_event.project_id)
case updated_event.source case updated_event.source
when 'repository' when 'repository'
......
...@@ -28,10 +28,16 @@ module Gitlab ...@@ -28,10 +28,16 @@ module Gitlab
# @return [Integer] id of last replicated event # @return [Integer] id of last replicated event
def self.last_processed def self.last_processed
last = ::Geo::EventLogState.last_processed.try(:id) last = ::Geo::EventLogState.last_processed&.id
return last if last return last if last
::Geo::EventLog.any? ? ::Geo::EventLog.last.id : -1 if ::Geo::EventLog.any?
event_id = ::Geo::EventLog.last.id
save_processed(event_id)
event_id
else
-1
end
end end
# private methods # private methods
......
module Gitlab module Gitlab
module PerformanceBar module PerformanceBar
def self.enabled? def self.enabled?
Feature.enabled?('gitlab_performance_bar') Rails.env.development? || Feature.enabled?('gitlab_performance_bar')
end end
end end
end end
module Gitlab
module SlashCommands
class ApplicationHelp < BaseCommand
def initialize(params)
@params = params
end
def execute
Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, params[:text])
end
private
def trigger
"#{params[:command]} [project name or alias]"
end
def commands
Gitlab::SlashCommands::Command::COMMANDS
end
end
end
end
module Gitlab
module SlashCommands
module Presenters
class Error < Presenters::Base
def initialize(message)
@message = message
end
def message
ephemeral_response(text: @message)
end
end
end
end
end
...@@ -41,9 +41,14 @@ module Peek ...@@ -41,9 +41,14 @@ module Peek
] ]
end.sort_by{ |a,b,c,d,e,f| -f } end.sort_by{ |a,b,c,d,e,f| -f }
output = '' output = "<div class='modal-dialog modal-full'><div class='modal-content'>"
per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort| output << "<div class='modal-header'>"
output << "<button class='close btn btn-link btn-sm' type='button' data-dismiss='modal'>X</button>"
output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>"
output << "</div>"
output << "<div class='modal-body'>"
per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort|
output << "<div class='peek-rblineprof-file'><div class='heading'>" output << "<div class='peek-rblineprof-file'><div class='heading'>"
show_src = file_sort > min show_src = file_sort > min
...@@ -86,11 +91,32 @@ module Peek ...@@ -86,11 +91,32 @@ module Peek
output << "</div></div>" # .data then .peek-rblineprof-file output << "</div></div>" # .data then .peek-rblineprof-file
end end
response.body += "<div class='peek-rblineprof-modal' id='line-profile'>#{output}</div>".html_safe output << "</div></div></div>"
response.body += "<div class='modal' id='modal-peek-line-profile' tabindex=-1>#{output}</div>".html_safe
end end
ret ret
end end
private
def human_description(lineprofiler_param)
case lineprofiler_param
when 'app'
'app/ & lib/'
when 'views'
'app/view/'
when 'gems'
'vendor/gems'
when 'all'
'everything in Rails.root'
when 'stdlib'
'everything in the Ruby standard library'
else
'app/, config/, lib/, vendor/ & plugin/'
end
end
end end
end end
end end
require 'spec_helper'
describe Projects::Settings::SlacksController do
let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) }
before do
project.team << [user, :master]
sign_in(user)
end
describe 'GET show' do
def redirect_url(project)
edit_project_service_path(
project,
project.build_gitlab_slack_application_service
)
end
def stub_service(result)
service = double
expect(service).to receive(:execute).and_return(result)
expect(Projects::SlackApplicationInstallService)
.to receive(:new).with(project, user, anything).and_return(service)
end
it 'calls service and redirects with no flash message if result is successful' do
stub_service(status: :success)
get :slack_auth, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(302)
expect(response).to redirect_to(redirect_url(project))
expect(flash[:alert]).to be_nil
end
it 'calls service and redirects with flash message if there is error' do
stub_service(status: :error, message: 'error')
get :slack_auth, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(302)
expect(response).to redirect_to(redirect_url(project))
expect(flash[:alert]).to eq('error')
end
end
end
...@@ -47,4 +47,9 @@ FactoryGirl.define do ...@@ -47,4 +47,9 @@ FactoryGirl.define do
type 'HipchatService' type 'HipchatService'
token 'test_token' token 'test_token'
end end
factory :gitlab_slack_application_service do
project factory: :empty_project
type 'GitlabSlackApplicationService'
end
end end
FactoryGirl.define do
factory :slack_integration do
sequence(:team_id) { |n| "T123#{n}" }
sequence(:user_id) { |n| "U123#{n}" }
sequence(:team_name) { |n| "team#{n}" }
sequence(:alias) { |n| "namespace#{n}/project_name#{n}" }
service factory: :gitlab_slack_application_service
end
end
require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:empty_project, :public) }
let!(:milestone) { create(:milestone, project: project) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], milestone: milestone, labels: [development], relative_position: 2) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { find('.board:nth-child(2)').first('.card') }
before do
Timecop.freeze
stub_licensed_features(multiple_issue_assignees: true)
project.team << [user, :master]
project.team.add_developer(user2)
gitlab_sign_in(user)
visit project_board_path(project, board)
wait_for_requests
end
after do
Timecop.return
end
context 'assignee' do
it 'updates the issues assignee' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
wait_for_requests
end
expect(page).to have_content(user.name)
end
expect(card).to have_selector('.avatar')
end
it 'adds multiple assignees' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
click_link user2.name
end
expect(page).to have_content(user.name)
expect(page).to have_content(user2.name)
end
expect(card.all('.avatar').length).to eq(2)
end
it 'removes the assignee' do
card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
click_card(card_two)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link 'Unassigned'
end
find('.dropdown-menu-toggle').click
wait_for_requests
expect(page).to have_content('No assignee')
end
expect(card_two).not_to have_selector('.avatar')
end
it 'assignees to current user' do
click_card(card)
page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
click_button 'assign yourself'
wait_for_requests
expect(page).to have_content(user.name)
end
expect(card).to have_selector('.avatar')
end
it 'updates assignee dropdown' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
wait_for_requests
end
expect(page).to have_content(user.name)
end
page.within(find('.board:nth-child(2)')) do
find('.card:nth-child(2)').trigger('click')
end
page.within('.assignee') do
click_link 'Edit'
expect(find('.dropdown-menu')).to have_selector('.is-active')
end
end
end
def click_card(card)
page.within(card) do
first('.card-number').click
end
wait_for_sidebar
end
def wait_for_sidebar
# loop until the CSS transition is complete
Timeout.timeout(0.5) do
loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
end
end
end
...@@ -17,9 +17,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -17,9 +17,9 @@ describe 'Issue Boards', feature: true, js: true do
before do before do
Timecop.freeze Timecop.freeze
stub_licensed_features(multiple_issue_assignees: false)
project.team << [user, :master] project.team << [user, :master]
project.team.add_developer(user2)
gitlab_sign_in(user) gitlab_sign_in(user)
...@@ -117,26 +117,6 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -117,26 +117,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(card).to have_selector('.avatar') expect(card).to have_selector('.avatar')
end end
it 'adds multiple assignees' do
click_card(card)
page.within('.assignee') do
click_link 'Edit'
wait_for_requests
page.within('.dropdown-menu-user') do
click_link user.name
click_link user2.name
end
expect(page).to have_content(user.name)
expect(page).to have_content(user2.name)
end
expect(card.all('.avatar').length).to eq(2)
end
it 'removes the assignee' do it 'removes the assignee' do
card_two = find('.board:nth-child(2)').find('.card:nth-child(2)') card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
click_card(card_two) click_card(card_two)
...@@ -150,8 +130,6 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -150,8 +130,6 @@ describe 'Issue Boards', feature: true, js: true do
click_link 'Unassigned' click_link 'Unassigned'
end end
find('.dropdown-menu-toggle').click
wait_for_requests wait_for_requests
expect(page).to have_content('No assignee') expect(page).to have_content('No assignee')
......
require 'rails_helper' require 'rails_helper'
describe 'New/edit issue (EE)', :feature, :js do describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
include FormHelper include FormHelper
...@@ -16,6 +16,8 @@ describe 'New/edit issue (EE)', :feature, :js do ...@@ -16,6 +16,8 @@ describe 'New/edit issue (EE)', :feature, :js do
before do before do
project.team << [user, :master] project.team << [user, :master]
project.team << [user2, :master] project.team << [user2, :master]
stub_licensed_features(multiple_issue_assignees: true)
gitlab_sign_in(user) gitlab_sign_in(user)
end end
...@@ -24,15 +26,15 @@ describe 'New/edit issue (EE)', :feature, :js do ...@@ -24,15 +26,15 @@ describe 'New/edit issue (EE)', :feature, :js do
visit new_project_issue_path(project) visit new_project_issue_path(project)
end end
describe 'shorten users API pagination limit (CE)' do describe 'shorten users API pagination limit' do
before do before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would # Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of # somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called. # the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes. # This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually. # To work around this, we have to hold on to and call to the original implementation manually.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args) options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2 options[:data][:per_page] = 2
...@@ -96,5 +98,174 @@ describe 'New/edit issue (EE)', :feature, :js do ...@@ -96,5 +98,174 @@ describe 'New/edit issue (EE)', :feature, :js do
expect(find('a', text: 'Assign to me')).to be_visible expect(find('a', text: 'Assign to me')).to be_visible
end end
end end
it 'allows user to create new issue' do
fill_in 'issue_title', with: 'title'
fill_in 'issue_description', with: 'title'
expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Unassigned'
wait_for_requests
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
expect(find('a', text: 'Assign to me')).to be_visible
click_link 'Assign to me'
assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
expect(assignee_ids[0].value).to match(user2.id.to_s)
expect(assignee_ids[1].value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content "#{user2.name} + 1 more"
end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
click_button 'Milestone'
page.within '.issue-milestone' do
click_link milestone.title
end
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
find('.dropdown-menu-close').click
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Weight'
page.within '.dropdown-menu-weight' do
click_link '1'
end
click_button 'Submit issue'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content "2 Assignees"
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
page.within '.weight' do
expect(page).to have_content '1'
end
end
page.within '.issuable-meta' do
issue = Issue.find_by(title: 'title')
expect(page).to have_text("Issue #{issue.to_reference}")
# compare paths because the host differ in test
expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
end
end
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
wait_for_requests
page.within '.dropdown-menu-user' do
click_link user.name
end
expect(find('.js-assignee-search')).to have_content(user.name)
page.within '.dropdown-menu-user' do
click_link user2.name
end
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[0].value).to match(user.id.to_s)
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[1].value).to match(user2.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active').length).to eq(2)
expect(page.all('.dropdown-menu-user a.is-active')[0].first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active')[1].first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
end
end
context 'edit issue' do
before do
visit edit_project_issue_path(project, issue)
end
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
end
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Save changes'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content user.name
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
end
end
end
def before_for_selector(selector)
js = <<-JS.strip_heredoc
(function(selector) {
var el = document.querySelector(selector);
return window.getComputedStyle(el, '::before').getPropertyValue('content');
})("#{escape_javascript(selector)}")
JS
page.evaluate_script(js)
end end
end end
...@@ -13,6 +13,8 @@ describe 'New/edit issue', :feature, :js do ...@@ -13,6 +13,8 @@ describe 'New/edit issue', :feature, :js do
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) } let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.team << [user, :master] project.team << [user, :master]
project.team << [user2, :master] project.team << [user2, :master]
gitlab_sign_in(user) gitlab_sign_in(user)
...@@ -23,15 +25,20 @@ describe 'New/edit issue', :feature, :js do ...@@ -23,15 +25,20 @@ describe 'New/edit issue', :feature, :js do
visit new_project_issue_path(project) visit new_project_issue_path(project)
end end
xdescribe 'shorten users API pagination limit (CE)' do describe 'shorten users API pagination limit' do
before do before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would # Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of # somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called. # the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes. # This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually. # To work around this, we have to hold on to and call to the original implementation manually.
<<<<<<< HEAD
original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options) original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args| allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
=======
original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
>>>>>>> upstream/master
options = original_issue_dropdown_options.bind(original.receiver).call(*args) options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2 options[:data][:per_page] = 2
...@@ -63,7 +70,7 @@ describe 'New/edit issue', :feature, :js do ...@@ -63,7 +70,7 @@ describe 'New/edit issue', :feature, :js do
end end
end end
xdescribe 'single assignee (CE)' do describe 'single assignee' do
before do before do
click_button 'Unassigned' click_button 'Unassigned'
...@@ -122,11 +129,10 @@ describe 'New/edit issue', :feature, :js do ...@@ -122,11 +129,10 @@ describe 'New/edit issue', :feature, :js do
click_link 'Assign to me' click_link 'Assign to me'
assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false) assignee_ids = page.all('input[name="issue[assignee_ids][]"]', visible: false)
expect(assignee_ids[0].value).to match(user2.id.to_s) expect(assignee_ids[0].value).to match(user.id.to_s)
expect(assignee_ids[1].value).to match(user.id.to_s)
page.within '.js-assignee-search' do page.within '.js-assignee-search' do
expect(page).to have_content "#{user2.name} + 1 more" expect(page).to have_content user.name
end end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
...@@ -152,17 +158,11 @@ describe 'New/edit issue', :feature, :js do ...@@ -152,17 +158,11 @@ describe 'New/edit issue', :feature, :js do
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Weight'
page.within '.dropdown-menu-weight' do
click_link '1'
end
click_button 'Submit issue' click_button 'Submit issue'
page.within '.issuable-sidebar' do page.within '.issuable-sidebar' do
page.within '.assignee' do page.within '.assignee' do
expect(page).to have_content "2 Assignees" expect(page).to have_content "Assignee"
end end
page.within '.milestone' do page.within '.milestone' do
...@@ -173,10 +173,6 @@ describe 'New/edit issue', :feature, :js do ...@@ -173,10 +173,6 @@ describe 'New/edit issue', :feature, :js do
expect(page).to have_content label.title expect(page).to have_content label.title
expect(page).to have_content label2.title expect(page).to have_content label2.title
end end
page.within '.weight' do
expect(page).to have_content '1'
end
end end
page.within '.issuable-meta' do page.within '.issuable-meta' do
...@@ -214,18 +210,13 @@ describe 'New/edit issue', :feature, :js do ...@@ -214,18 +210,13 @@ describe 'New/edit issue', :feature, :js do
end end
expect(find('.js-assignee-search')).to have_content(user.name) expect(find('.js-assignee-search')).to have_content(user.name)
click_button user.name
page.within '.dropdown-menu-user' do page.within '.dropdown-menu-user' do
click_link user2.name click_link user2.name
end end
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[0].value).to match(user.id.to_s) expect(find('.js-assignee-search')).to have_content(user2.name)
expect(page.all('input[name="issue[assignee_ids][]"]', visible: false)[1].value).to match(user2.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active').length).to eq(2)
expect(page.all('.dropdown-menu-user a.is-active')[0].first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
expect(page.all('.dropdown-menu-user a.is-active')[1].first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s)
end end
it 'description has autocomplete' do it 'description has autocomplete' do
......
...@@ -40,23 +40,11 @@ ...@@ -40,23 +40,11 @@
"additionalProperties": false "additionalProperties": false
} }
}, },
"assignees": { "assignee": {
"type": "array", "id": { "type": "integer" },
"items": { "name": { "type": "string" },
"type": ["object", "null"], "username": { "type": "string" },
"required": [ "avatar_url": { "type": "uri" }
"id",
"name",
"username",
"avatar_url"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
}
}
}, },
"assignees": { "assignees": {
"type": "array", "type": "array",
......
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"milestone": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"project_id": { "type": "integer" },
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"due_date": { "type": "date" },
"start_date": { "type": "date" }
},
"additionalProperties": false
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
}
},
"assignee": {
"type": ["object", "null"],
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"username": { "type": "string" },
"id": { "type": "integer" },
"state": { "type": "string" },
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
"additionalProperties": false
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"weight": { "type": ["integer", "null"] }
},
"required": [
"id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential",
"web_url", "weight"
],
"additionalProperties": false
}
}
...@@ -77,15 +77,14 @@ ...@@ -77,15 +77,14 @@
"downvotes": { "type": "integer" }, "downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] }, "due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"web_url": { "type": "uri" }, "web_url": { "type": "uri" }
"weight": { "type": ["integer", "null"] }
}, },
"required": [ "required": [
"id", "iid", "project_id", "title", "description", "id", "iid", "project_id", "title", "description",
"state", "created_at", "updated_at", "labels", "state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count", "milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential", "upvotes", "downvotes", "due_date", "confidential",
"web_url", "weight" "web_url"
], ],
"additionalProperties": false "additionalProperties": false
} }
......
...@@ -15,6 +15,9 @@ import '~/filtered_search/filtered_search_token_keys_issues_ee'; ...@@ -15,6 +15,9 @@ import '~/filtered_search/filtered_search_token_keys_issues_ee';
let tokenKeys; let tokenKeys;
beforeEach(() => { beforeEach(() => {
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: true,
});
tokenKeys = gl.FilteredSearchTokenKeysIssuesEE.get(); tokenKeys = gl.FilteredSearchTokenKeysIssuesEE.get();
}); });
......
...@@ -2,10 +2,6 @@ require 'spec_helper' ...@@ -2,10 +2,6 @@ require 'spec_helper'
describe Gitlab::Geo::LogCursor::Daemon, lib: true do describe Gitlab::Geo::LogCursor::Daemon, lib: true do
describe '#run!' do describe '#run!' do
before do
allow(subject).to receive(:exit?) { true }
end
it 'traps signals' do it 'traps signals' do
allow(subject).to receive(:exit?) { true } allow(subject).to receive(:exit?) { true }
expect(subject).to receive(:trap_signals) expect(subject).to receive(:trap_signals)
...@@ -17,10 +13,44 @@ describe Gitlab::Geo::LogCursor::Daemon, lib: true do ...@@ -17,10 +13,44 @@ describe Gitlab::Geo::LogCursor::Daemon, lib: true do
subject { described_class.new(full_scan: true) } subject { described_class.new(full_scan: true) }
it 'executes a full-scan' do it 'executes a full-scan' do
allow(subject).to receive(:exit?) { true }
expect(subject).to receive(:full_scan!) expect(subject).to receive(:full_scan!)
subject.run! subject.run!
end end
end end
context 'when processing a repository updated event' do
let(:event_log) { create(:geo_event_log) }
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log.id - 1) }
let(:repository_updated_event) { event_log.repository_updated_event }
before do
allow(subject).to receive(:exit?).and_return(false, true)
end
it 'creates a new project registry if it does not exist' do
expect { subject.run! }.to change(Geo::ProjectRegistry, :count).by(1)
end
it 'sets resync_repository to true if event source is repository' do
repository_updated_event.update_attribute(:source, Geo::RepositoryUpdatedEvent::REPOSITORY)
registry = create(:geo_project_registry, :synced, project: repository_updated_event.project)
subject.run!
expect(registry.reload.resync_repository).to be true
end
it 'sets resync_wiki to true if event source is wiki' do
repository_updated_event.update_attribute(:source, Geo::RepositoryUpdatedEvent::WIKI)
registry = create(:geo_project_registry, :synced, project: repository_updated_event.project)
subject.run!
expect(registry.reload.resync_wiki).to be true
end
end
end end
end end
...@@ -2,20 +2,27 @@ require 'spec_helper' ...@@ -2,20 +2,27 @@ require 'spec_helper'
describe Gitlab::Geo::LogCursor::Events, lib: true do describe Gitlab::Geo::LogCursor::Events, lib: true do
describe '.fetch_in_batches' do describe '.fetch_in_batches' do
let!(:event_log) { create(:geo_event_log) } let!(:event_log_1) { create(:geo_event_log) }
let!(:event_log_2) { create(:geo_event_log) }
before do context 'when no event_log_state exist' do
allow(described_class).to receive(:last_processed) { -1 } it 'does not yield a group of events' do
expect { |b| described_class.fetch_in_batches(&b) }.not_to yield_with_args([event_log_1, event_log_2])
end
end end
it 'yields a group of events' do context 'when there is already an event_log_state' do
expect { |b| described_class.fetch_in_batches(&b) }.to yield_with_args([event_log]) let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log_1.id - 1) }
end
it 'yields a group of events' do
expect { |b| described_class.fetch_in_batches(&b) }.to yield_with_args([event_log_1, event_log_2])
end
it 'saves processed files after yielding' do it 'saves last event as last processed after yielding' do
expect(described_class).to receive(:save_processed) described_class.fetch_in_batches { |batch| batch }
described_class.fetch_in_batches { |batch| batch } expect(Geo::EventLogState.last.event_id).to eq(event_log_2.id)
end
end end
it 'skips execution if cannot achieve a lease' do it 'skips execution if cannot achieve a lease' do
...@@ -26,15 +33,15 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do ...@@ -26,15 +33,15 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do
end end
describe '.save_processed' do describe '.save_processed' do
it 'saves a new entry in geo_event_log_state' do it 'creates a new event_log_state when no event_log_state exist' do
expect { described_class.save_processed(1) }.to change(Geo::EventLogState, :count).by(1) expect { described_class.save_processed(1) }.to change(Geo::EventLogState, :count).by(1)
expect(Geo::EventLogState.last.event_id).to eq(1) expect(Geo::EventLogState.last.event_id).to eq(1)
end end
it 'removes older entries from geo_event_log_state' do it 'updates the event_id when there is already an event_log_state' do
create(:geo_event_log_state) create(:geo_event_log_state)
expect { described_class.save_processed(2) }.to change(Geo::EventLogState, :count).by(0) expect { described_class.save_processed(2) }.not_to change(Geo::EventLogState, :count)
expect(Geo::EventLogState.last.event_id).to eq(2) expect(Geo::EventLogState.last.event_id).to eq(2)
end end
end end
...@@ -52,6 +59,11 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do ...@@ -52,6 +59,11 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do
it 'returns last event id' do it 'returns last event id' do
expect(described_class.last_processed).to eq(event_log.id) expect(described_class.last_processed).to eq(event_log.id)
end end
it 'saves last event as the last processed' do
expect { described_class.last_processed }.to change(Geo::EventLogState, :count).by(1)
expect(Geo::EventLogState.last.event_id).to eq(event_log.id)
end
end end
context 'when there is already an event_log_state' do context 'when there is already an event_log_state' do
......
...@@ -190,6 +190,7 @@ project: ...@@ -190,6 +190,7 @@ project:
- pipelines_email_service - pipelines_email_service
- mattermost_slash_commands_service - mattermost_slash_commands_service
- slack_slash_commands_service - slack_slash_commands_service
- gitlab_slack_application_service
- irker_service - irker_service
- pivotaltracker_service - pivotaltracker_service
- prometheus_service - prometheus_service
......
require 'spec_helper'
describe Gitlab::SlashCommands::ApplicationHelp, service: true do
let(:params) { { command: '/gitlab', text: 'help' } }
describe '#execute' do
subject do
described_class.new(params).execute
end
it 'displays the help section' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to include('Available commands')
expect(subject[:text]).to include('/gitlab [project name or alias] issue show')
end
end
end
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::Error do
subject { described_class.new('Error').message }
it { is_expected.to be_a(Hash) }
it 'shows the error message' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:status]).to eq(200)
expect(subject[:text]).to eq('Error')
end
end
require 'spec_helper' require 'spec_helper'
describe Issue do describe Issue do
describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: false)
issue = build(:issue)
expect(issue.allows_multiple_assignees?).to be_falsey
end
it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: true)
issue = build(:issue)
expect(issue.allows_multiple_assignees?).to be_truthy
end
end
describe '#weight' do describe '#weight' do
[ [
{ license: true, database: 5, expected: 5 }, { license: true, database: 5, expected: 5 },
......
...@@ -42,6 +42,34 @@ describe Namespace, models: true do ...@@ -42,6 +42,34 @@ describe Namespace, models: true do
end end
end end
describe '#move_dir' do
context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) }
let(:gitlab_shell) { Gitlab::Shell.new }
it 'logs the Geo::RepositoryRenamedEvent for each project inside namespace' do
parent = create(:namespace)
child = create(:group, name: 'child', path: 'child', parent: parent)
project_1 = create(:project_empty_repo, namespace: parent)
create(:project_empty_repo, namespace: child)
full_path_was = "#{parent.full_path}_old"
new_path = parent.full_path
allow(parent).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(parent).to receive(:path_changed?).and_return(true)
allow(parent).to receive(:full_path_was).and_return(full_path_was)
allow(parent).to receive(:full_path).and_return(new_path)
allow(gitlab_shell).to receive(:mv_namespace)
.ordered
.with(project_1.repository_storage_path, full_path_was, new_path)
.and_return(true)
expect { parent.move_dir }.to change(Geo::RepositoryRenamedEvent, :count).by(2)
end
end
end
describe '#feature_available?' do describe '#feature_available?' do
let(:plan_license) { Namespace::BRONZE_PLAN } let(:plan_license) { Namespace::BRONZE_PLAN }
let(:group) { create(:group, plan: plan_license) } let(:group) { create(:group, plan: plan_license) }
......
...@@ -639,4 +639,37 @@ describe Project, models: true do ...@@ -639,4 +639,37 @@ describe Project, models: true do
end end
end end
end end
describe '#rename_repo' do
context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) }
let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
before do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end
it 'logs the Geo::RepositoryRenamedEvent' do
stub_container_registry_config(enabled: false)
allow(gitlab_shell).to receive(:mv_repository)
.ordered
.with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}")
.and_return(true)
allow(gitlab_shell).to receive(:mv_repository)
.ordered
.with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
.and_return(true)
expect(Geo::RepositoryRenamedEventStore).to receive(:new)
.with(instance_of(Project), old_path: 'foo', old_path_with_namespace: "#{project.namespace.full_path}/foo")
.and_call_original
expect { project.rename_repo }.to change(Geo::RepositoryRenamedEvent, :count).by(1)
end
end
end
end end
...@@ -3,5 +3,6 @@ require 'spec_helper' ...@@ -3,5 +3,6 @@ require 'spec_helper'
RSpec.describe Geo::EventLog, type: :model do RSpec.describe Geo::EventLog, type: :model do
describe 'relationships' do describe 'relationships' do
it { is_expected.to belong_to(:repository_updated_event).class_name('Geo::RepositoryUpdatedEvent').with_foreign_key('repository_updated_event_id') } it { is_expected.to belong_to(:repository_updated_event).class_name('Geo::RepositoryUpdatedEvent').with_foreign_key('repository_updated_event_id') }
it { is_expected.to belong_to(:repository_renamed_event).class_name('Geo::RepositoryRenamedEvent').with_foreign_key('repository_renamed_event_id') }
end end
end end
require 'spec_helper'
RSpec.describe Geo::RepositoryRenamedEvent, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:repository_storage_name) }
it { is_expected.to validate_presence_of(:repository_storage_path) }
it { is_expected.to validate_presence_of(:old_path_with_namespace) }
it { is_expected.to validate_presence_of(:new_path_with_namespace) }
it { is_expected.to validate_presence_of(:old_wiki_path_with_namespace) }
it { is_expected.to validate_presence_of(:new_wiki_path_with_namespace) }
it { is_expected.to validate_presence_of(:old_path) }
it { is_expected.to validate_presence_of(:new_path) }
end
end
...@@ -212,9 +212,19 @@ describe License do ...@@ -212,9 +212,19 @@ describe License do
end end
describe '.features_for_plan' do describe '.features_for_plan' do
it 'returns features for given plan' do it 'returns features for starter plan' do
expect(described_class.features_for_plan('starter'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1 })
end
it 'returns features for premium plan' do
expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_MultipleIssueAssignees' => 1, 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 })
end
it 'returns features for early adopter plan' do
expect(described_class.features_for_plan('premium')) expect(described_class.features_for_plan('premium'))
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 }) .to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 } )
end end
it 'returns empty Hash if no features for given plan' do it 'returns empty Hash if no features for given plan' do
......
require 'spec_helper'
describe SlackIntegration, models: true do
describe "Associations" do
it { is_expected.to belong_to(:service) }
end
describe 'Validations' do
it { is_expected.to validate_presence_of(:team_id) }
it { is_expected.to validate_presence_of(:team_name) }
it { is_expected.to validate_presence_of(:alias) }
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:service) }
end
end
require 'spec_helper'
describe API::Issues do # rubocop:disable RSpec/FilePath
include EmailHelpers
set(:user) { create(:user) }
set(:project) do
create(:empty_project, :public, creator_id: user.id, namespace: user.namespace)
end
let(:user2) { create(:user) }
set(:author) { create(:author) }
set(:assignee) { create(:assignee) }
let(:issue_title) { 'foo' }
let(:issue_description) { 'closed' }
let!(:issue) do
create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
end
set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before(:all) do
project.team << [user, :reporter]
end
describe "GET /issues" do
context "when authenticated" do
it 'matches V4 response schema' do
get api('/issues', user)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('public_api/v4/ee/issues')
end
end
end
describe "POST /projects/:id/issues" do
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
assignee_ids: [user2.id]
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
end
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to eq(5)
end
it 'removes a weight from an issue' do
weighted_issue = create(:issue, project: project, weight: 2)
put api("/projects/#{project.id}/issues/#{weighted_issue.iid}", user), weight: nil
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
end
it 'returns 400 if weight is less than minimum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: -1
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
it 'returns 400 if weight is more than maximum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 10
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
context 'issuable weights unlicensed' do
before do
stub_licensed_features(issue_weights: false)
end
it 'ignores the update' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
expect(issue.reload.read_attribute(:weight)).to be_nil
end
end
end
def expect_paginated_array_response(size: nil)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(size) if size
end
end
...@@ -63,6 +63,10 @@ describe API::Issues do ...@@ -63,6 +63,10 @@ describe API::Issues do
project.team << [guest, :guest] project.team << [guest, :guest]
end end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe "GET /issues" do describe "GET /issues" do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authentication error" do
...@@ -691,7 +695,6 @@ describe API::Issues do ...@@ -691,7 +695,6 @@ describe API::Issues do
expect(json_response['assignee']).to be_a Hash expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to be_nil
end end
it "returns a project issue by internal id" do it "returns a project issue by internal id" do
...@@ -773,6 +776,17 @@ describe API::Issues do ...@@ -773,6 +776,17 @@ describe API::Issues do
end end
end end
context 'single assignee restrictions' do
it 'creates a new project issue with no more than one assignee' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', assignee_ids: [user2.id, guest.id]
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new issue')
expect(json_response['assignees'].count).to eq(1)
end
end
it 'creates a new project issue' do it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user), post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3, title: 'new issue', labels: 'label, label2', weight: 3,
...@@ -783,7 +797,6 @@ describe API::Issues do ...@@ -783,7 +797,6 @@ describe API::Issues do
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['confidential']).to be_falsy expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to eq(3)
expect(json_response['assignee']['name']).to eq(user2.name) expect(json_response['assignee']['name']).to eq(user2.name)
expect(json_response['assignees'].first['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name)
end end
...@@ -1113,6 +1126,17 @@ describe API::Issues do ...@@ -1113,6 +1126,17 @@ describe API::Issues do
expect(json_response['assignees'].first['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name)
end end
context 'single assignee restrictions' do
it 'updates an issue with several assignees but only one has been applied' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user),
assignee_ids: [user2.id, guest.id]
expect(response).to have_http_status(200)
expect(json_response['assignees'].size).to eq(1)
end
end
end end
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
...@@ -1218,52 +1242,6 @@ describe API::Issues do ...@@ -1218,52 +1242,6 @@ describe API::Issues do
end end
end end
describe 'PUT /projects/:id/issues/:issue_id to update weight' do
it 'updates an issue with no weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to eq(5)
end
it 'removes a weight from an issue' do
weighted_issue = create(:issue, project: project, weight: 2)
put api("/projects/#{project.id}/issues/#{weighted_issue.iid}", user), weight: nil
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
end
it 'returns 400 if weight is less than minimum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: -1
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
it 'returns 400 if weight is more than maximum weight' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 10
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('weight does not have a valid value')
end
context 'issuable weights unlicensed' do
before do
stub_licensed_features(issue_weights: false)
end
it 'ignores the update' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), weight: 5
expect(response).to have_http_status(200)
expect(json_response['weight']).to be_nil
expect(issue.reload.read_attribute(:weight)).to be_nil
end
end
end
describe "DELETE /projects/:id/issues/:issue_iid" do describe "DELETE /projects/:id/issues/:issue_iid" do
it "rejects a non member from deleting an issue" do it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member) delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
......
...@@ -11,6 +11,8 @@ describe API::Milestones do ...@@ -11,6 +11,8 @@ describe API::Milestones do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
stub_licensed_features(issue_weights: false)
end end
describe 'GET /projects/:id/milestones' do describe 'GET /projects/:id/milestones' do
......
...@@ -174,4 +174,21 @@ describe API::Services do ...@@ -174,4 +174,21 @@ describe API::Services do
end end
end end
end end
describe 'Slack application Service' do
before do
project.create_gitlab_slack_application_service
stub_application_setting(
slack_app_verification_token: 'token'
)
end
it 'returns status 200' do
post api('/slack/trigger'), token: 'token', text: 'help'
expect(response).to have_http_status(200)
expect(json_response['response_type']).to eq("ephemeral")
end
end
end end
...@@ -10,32 +10,22 @@ describe Projects::DestroyService, services: true do ...@@ -10,32 +10,22 @@ describe Projects::DestroyService, services: true do
let!(:wiki_path) { project.path_with_namespace + '.wiki' } let!(:wiki_path) { project.path_with_namespace + '.wiki' }
let!(:storage_name) { project.repository_storage } let!(:storage_name) { project.repository_storage }
let!(:storage_path) { project.repository_storage_path } let!(:storage_path) { project.repository_storage_path }
let!(:geo_node) { create(:geo_node, :primary, :current) }
subject { described_class.new(project, user, {}) }
before do before do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: []) stub_container_registry_tags(repository: :any, tags: [])
end end
context 'Geo primary' do context 'when running on a primary node' do
it 'logs the event' do let!(:geo_node) { create(:geo_node, :primary, :current) }
# Run sidekiq immediatly to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
event = Geo::RepositoryDeletedEvent.first
expect(Geo::EventLog.count).to eq(1) it 'logs an event to the Geo event log' do
expect(Geo::RepositoryDeletedEvent.count).to eq(1) # Run Sidekiq immediately to check that renamed repository will be removed
expect(event.project_id).to eq(project_id) Sidekiq::Testing.inline! do
expect(event.deleted_path).to eq(project_path) expect { subject.execute }.to change(Geo::RepositoryDeletedEvent, :count).by(1)
expect(event.deleted_wiki_path).to eq(wiki_path) end
expect(event.deleted_project_name).to eq(project_name)
expect(event.repository_storage_name).to eq(storage_name)
expect(event.repository_storage_path).to eq(storage_path)
end end
end end
def destroy_project(project, user, params = {})
described_class.new(project, user, params).execute
end
end end
# rubocop:disable RSpec/FilePath
require 'spec_helper'
describe Projects::TransferService, services: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
subject { described_class.new(project, user) }
before do
group.add_owner(user)
end
context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) }
it 'logs an event to the Geo event log' do
expect { subject.execute(group) }.to change(Geo::RepositoryRenamedEvent, :count).by(1)
end
end
end
require 'spec_helper'
describe QuickActions::InterpretService, services: true do # rubocop:disable RSpec/FilePath
let(:user) { create(:user) }
let(:developer) { create(:user) }
let(:developer2) { create(:user) }
let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(project, developer) }
before do
stub_licensed_features(multiple_issue_assignees: true)
project.add_developer(developer)
end
describe '#execute' do
context 'assign command' do
let(:content) { "/assign @#{developer.username}" }
context 'Issue' do
it 'fetches assignees and populates them if content contains /assign' do
issue.assignees << user
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, user.id])
end
context 'assign command with multiple assignees' do
let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
before do
project.add_developer(developer2)
end
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id])
end
end
end
end
context 'unassign command' do
let(:content) { '/unassign' }
context 'Issue' do
it 'unassigns user if content contains /unassign @user' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute("/unassign @#{developer2.username}", issue)
expect(updates).to eq(assignee_ids: [developer.id])
end
it 'unassigns both users if content contains /unassign @user @user1' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id, user.id])
_, updates = service.execute("/unassign @#{developer2.username} @#{developer.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
it 'unassigns all the users if content contains /unassign' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute('/unassign', issue)
expect(updates[:assignee_ids]).to be_empty
end
end
end
context 'reassign command' do
let(:content) { "/reassign @#{user.username}" }
context 'Merge Request' do
let(:merge_request) { create(:merge_request, source_project: project) }
it 'does not recognize /reassign @user' do
_, updates = service.execute(content, merge_request)
expect(updates).to be_empty
end
end
context 'Issue' do
let(:content) { "/reassign @#{user.username}" }
before do
issue.update(assignee_ids: [developer.id])
end
context 'unlicensed' do
before do
stub_licensed_features(multiple_issue_assignees: false)
end
it 'does not recognize /reassign @user' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
it 'reassigns user if content contains /reassign @user' do
_, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
end
end
end
end
require 'spec_helper'
describe Geo::RepositoryDeletedEventStore, services: true do
let(:project) { create(:empty_project, path: 'bar') }
let!(:project_id) { project.id }
let!(:project_name) { project.name }
let!(:repo_path) { project.full_path }
let!(:wiki_path) { "#{project.full_path}.wiki" }
let!(:storage_name) { project.repository_storage }
let!(:storage_path) { project.repository_storage_path }
subject { described_class.new(project, repo_path: repo_path, wiki_path: wiki_path) }
describe '#create' do
it 'does not create an event when not running on a primary node' do
allow(Gitlab::Geo).to receive(:primary?) { false }
expect { subject.create }.not_to change(Geo::RepositoryDeletedEvent, :count)
end
context 'when running on a primary node' do
before do
allow(Gitlab::Geo).to receive(:primary?) { true }
end
it 'creates a deleted event' do
expect { subject.create }.to change(Geo::RepositoryDeletedEvent, :count).by(1)
end
it 'tracks information for the deleted project' do
subject.create
event = Geo::RepositoryDeletedEvent.last
expect(event.project_id).to eq(project_id)
expect(event.deleted_path).to eq(repo_path)
expect(event.deleted_wiki_path).to eq(wiki_path)
expect(event.deleted_project_name).to eq(project_name)
expect(event.repository_storage_name).to eq(storage_name)
expect(event.repository_storage_path).to eq(storage_path)
end
end
end
end
require 'spec_helper'
describe Geo::RepositoryRenamedEventStore, services: true do
let(:project) { create(:empty_project, path: 'bar') }
let(:old_path) { 'foo' }
let(:old_path_with_namespace) { "#{project.namespace.full_path}/foo" }
subject { described_class.new(project, old_path: old_path, old_path_with_namespace: old_path_with_namespace) }
describe '#create' do
it 'does not create an event when not running on a primary node' do
allow(Gitlab::Geo).to receive(:primary?) { false }
expect { subject.create }.not_to change(Geo::RepositoryRenamedEvent, :count)
end
context 'when running on a primary node' do
before do
allow(Gitlab::Geo).to receive(:primary?) { true }
end
it 'creates a renamed event' do
expect { subject.create }.to change(Geo::RepositoryRenamedEvent, :count).by(1)
end
it 'tracks old and new paths for project repositories' do
subject.create
event = Geo::RepositoryRenamedEvent.last
expect(event.repository_storage_name).to eq(project.repository_storage)
expect(event.repository_storage_path).to eq(project.repository_storage_path)
expect(event.old_path_with_namespace).to eq(old_path_with_namespace)
expect(event.new_path_with_namespace).to eq(project.full_path)
expect(event.old_wiki_path_with_namespace).to eq("#{old_path_with_namespace}.wiki")
expect(event.new_wiki_path_with_namespace).to eq("#{project.full_path}.wiki")
expect(event.old_path).to eq(old_path)
expect(event.new_path).to eq(project.path)
end
end
end
end
require 'spec_helper'
describe Projects::SlackApplicationInstallService, services: true do
let!(:user) { create(:user) }
let!(:project) { create(:project) }
def service(params = {})
Projects::SlackApplicationInstallService.new(project, user, params)
end
def stub_slack_response_with(response)
expect_any_instance_of(Projects::SlackApplicationInstallService)
.to receive(:exchange_slack_token).and_return(response.stringify_keys)
end
def expect_slack_integration_is_created(project)
integration = SlackIntegration.find_by(service_id: project.gitlab_slack_application_service.id)
expect(integration).to be_present
end
def expect_chat_name_is_created(project)
chat_name = ChatName.find_by(service_id: project.gitlab_slack_application_service.id)
expect(chat_name).to be_present
end
it 'returns error result' do
stub_slack_response_with(ok: false, error: 'something is wrong')
result = service.execute
expect(result).to eq(message: 'Slack: something is wrong', status: :error)
end
it 'returns success result and creates all the needed records' do
stub_slack_response_with(
ok: true,
access_token: 'XXXX',
user_id: 'U12345',
team_id: 'T1265',
team_name: 'super-team'
)
result = service.execute
expect(result).to eq(status: :success)
expect_slack_integration_is_created(project)
expect_chat_name_is_created(project)
end
end
...@@ -37,7 +37,7 @@ describe Projects::TransferService, services: true do ...@@ -37,7 +37,7 @@ describe Projects::TransferService, services: true do
end end
it 'executes system hooks' do it 'executes system hooks' do
expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks) expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(project, :transfer)
transfer_project(project, user, group) transfer_project(project, user, group)
end end
...@@ -80,7 +80,7 @@ describe Projects::TransferService, services: true do ...@@ -80,7 +80,7 @@ describe Projects::TransferService, services: true do
end end
it "doesn't run system hooks" do it "doesn't run system hooks" do
expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks) expect_any_instance_of(SystemHooksService).not_to receive(:execute_hooks_for).with(project, :transfer)
attempt_project_transfer attempt_project_transfer
end end
......
...@@ -11,6 +11,8 @@ describe QuickActions::InterpretService, services: true do ...@@ -11,6 +11,8 @@ describe QuickActions::InterpretService, services: true do
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do before do
stub_licensed_features(multiple_issue_assignees: false)
project.team << [developer, :developer] project.team << [developer, :developer]
end end
...@@ -399,21 +401,15 @@ describe QuickActions::InterpretService, services: true do ...@@ -399,21 +401,15 @@ describe QuickActions::InterpretService, services: true do
let(:content) { "/assign @#{developer.username}" } let(:content) { "/assign @#{developer.username}" }
context 'Issue' do context 'Issue' do
it 'fetches assignees and populates them if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
user = create(:user)
issue.assignees << user
_, updates = service.execute(content, issue) _, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, user.id]) expect(updates[:assignee_ids]).to match_array([developer.id])
end end
end end
context 'Merge Request' do context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
user = create(:user)
merge_request.update(assignee: user)
_, updates = service.execute(content, merge_request) _, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_ids: [developer.id]) expect(updates).to eq(assignee_ids: [developer.id])
...@@ -432,7 +428,7 @@ describe QuickActions::InterpretService, services: true do ...@@ -432,7 +428,7 @@ describe QuickActions::InterpretService, services: true do
it 'fetches assignee and populates assignee_ids if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, issue) _, updates = service.execute(content, issue)
expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id]) expect(updates[:assignee_ids]).to match_array([developer.id])
end end
end end
...@@ -459,46 +455,11 @@ describe QuickActions::InterpretService, services: true do ...@@ -459,46 +455,11 @@ describe QuickActions::InterpretService, services: true do
let(:content) { '/unassign' } let(:content) { '/unassign' }
context 'Issue' do context 'Issue' do
it 'unassigns user if content contains /unassign @user' do it 'populates assignee_ids: [] if content contains /unassign' do
issue.update(assignee_ids: [developer.id, developer2.id]) issue.update(assignee_ids: [developer.id])
_, updates = service.execute(content, issue)
_, updates = service.execute("/unassign @#{developer2.username}", issue)
expect(updates).to eq(assignee_ids: [developer.id])
end
it 'unassigns both users if content contains /unassign @user @user1' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id, user.id])
_, updates = service.execute("/unassign @#{developer2.username} @#{developer.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
end
it 'unassigns all the users if content contains /unassign' do
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute('/unassign', issue)
expect(updates[:assignee_ids]).to be_empty
end
end
context 'reassign command' do
let(:content) { '/reassign' }
context 'Issue' do
it 'reassigns user if content contains /reassign @user' do
user = create(:user)
issue.update(assignee_ids: [developer.id, developer2.id])
_, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id]) expect(updates).to eq(assignee_ids: [])
end
end end
end end
...@@ -508,6 +469,7 @@ describe QuickActions::InterpretService, services: true do ...@@ -508,6 +469,7 @@ describe QuickActions::InterpretService, services: true do
_, updates = service.execute(content, merge_request) _, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_ids: []) expect(updates).to eq(assignee_ids: [])
<<<<<<< HEAD
end end
end end
end end
...@@ -524,6 +486,8 @@ describe QuickActions::InterpretService, services: true do ...@@ -524,6 +486,8 @@ describe QuickActions::InterpretService, services: true do
_, updates = service.execute("/reassign @#{user.username}", issue) _, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id]) expect(updates).to eq(assignee_ids: [user.id])
=======
>>>>>>> upstream/master
end end
end end
end end
...@@ -1016,7 +980,7 @@ describe QuickActions::InterpretService, services: true do ...@@ -1016,7 +980,7 @@ describe QuickActions::InterpretService, services: true do
it 'includes current assignee reference' do it 'includes current assignee reference' do
_, explanations = service.explain(content, issue) _, explanations = service.explain(content, issue)
expect(explanations).to eq(["Removes assignee #{developer.to_reference}"]) expect(explanations).to eq(["Removes assignee @#{developer.username}."])
end end
end end
......
require 'spec_helper'
describe SlashCommands::GlobalSlackHandler, service: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:verification_token) { '123' }
before do
stub_application_setting(
slack_app_verification_token: verification_token
)
end
def enable_slack_application(project)
create(:gitlab_slack_application_service, project: project)
end
def handler(params)
SlashCommands::GlobalSlackHandler.new(params)
end
def handler_with_valid_token(params)
handler(params.merge(token: verification_token))
end
it 'does not serve a request if token is invalid' do
result = handler(token: '123456', text: 'help').trigger
expect(result).to be_falsey
end
context 'Valid token' do
it 'calls command handler if project alias is valid' do
expect_any_instance_of(Gitlab::SlashCommands::Command).to receive(:execute)
expect_any_instance_of(ChatNames::FindUserService).to receive(:execute).and_return(user)
enable_slack_application(project)
slack_integration = create(:slack_integration, service: project.gitlab_slack_application_service)
slack_integration.update(alias: project.path_with_namespace)
handler_with_valid_token(
text: "#{project.path_with_namespace} issue new title",
team_id: slack_integration.team_id
).trigger
end
it 'returns error if project alias not found' do
expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute)
expect_any_instance_of(Gitlab::SlashCommands::Presenters::Error).to receive(:message)
enable_slack_application(project)
slack_integration = create(:slack_integration, service: project.gitlab_slack_application_service)
handler_with_valid_token(
text: "fake/fake issue new title",
team_id: slack_integration.team_id
).trigger
end
it 'returns authorization request' do
expect_any_instance_of(ChatNames::AuthorizeUserService).to receive(:execute)
expect_any_instance_of(Gitlab::SlashCommands::Presenters::Access).to receive(:authorize)
enable_slack_application(project)
slack_integration = create(:slack_integration, service: project.gitlab_slack_application_service)
slack_integration.update(alias: project.path_with_namespace)
handler_with_valid_token(
text: "#{project.path_with_namespace} issue new title",
team_id: slack_integration.team_id
).trigger
end
it 'calls help presenter' do
expect_any_instance_of(Gitlab::SlashCommands::ApplicationHelp).to receive(:execute)
handler_with_valid_token(
text: "help"
).trigger
end
end
end
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