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.
## 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)
......
......@@ -12,7 +12,7 @@ entry.
## 9.3.4 (2017-07-03)
- No changes.
- Update gitlab-shell to 5.1.1 !12615
## 9.3.3 (2017-06-30)
......
......@@ -60,6 +60,7 @@ import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
// EE-only
import ApproversSelect from './approvers_select';
......@@ -541,6 +542,10 @@ import AuditLogs from './audit_logs';
if (!shortcut_handler) {
new Shortcuts();
}
if (document.querySelector('#peek')) {
new PerformanceBar({ container: '#peek' });
}
};
Dispatcher.prototype.initSearch = function() {
......
......@@ -15,6 +15,10 @@ class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: this.filteredSearchInput.dataset.multipleAssignees,
});
if (this.page === 'issues' || this.page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysIssuesEE;
}
......
......@@ -19,12 +19,18 @@ const weightConditions = [{
}];
class FilteredSearchTokenKeysIssuesEE extends gl.FilteredSearchTokenKeys {
static init(availableFeatures) {
this.availableFeatures = availableFeatures;
}
static get() {
const tokenKeys = Array.from(super.get());
// Enable multiple assignees
const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee');
assigneeTokenKey.type = 'array';
// Enable multiple assignees when available
if (this.availableFeatures && this.availableFeatures.multipleAssignees) {
const assigneeTokenKey = tokenKeys.find(tk => tk.key === 'assignee');
assigneeTokenKey.type = 'array';
}
tokenKeys.push(weightTokenKey);
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 {
saveAssignees() {
this.loading = true;
function setLoadingFalse() {
this.loading = false;
}
this.mediator.saveAssignees(this.field)
.then(() => {
this.loading = false;
})
.then(setLoadingFalse.bind(this))
.catch(() => {
this.loading = false;
setLoadingFalse();
return new Flash('Error occurred when saving assignees');
});
},
......
......@@ -21,3 +21,9 @@ body.modal-open {
width: 860px;
}
}
@media (min-width: $screen-lg-min) {
.modal-full {
width: 98%;
}
}
......@@ -614,3 +614,15 @@ Convdev Index
$color-high-score: $green-400;
$color-average-score: $orange-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
//= require peek/views/rblineprof
header.navbar-gitlab.with-peek {
top: 35px;
}
@import "framework/variables";
@import "peek/views/performance_bar";
@import "peek/views/rblineprof";
#peek {
height: 35px;
background: #000;
background: $black;
line-height: 35px;
color: #999;
color: $perf-bar-text;
&.disabled {
display: none;
}
&.production {
background-color: #222;
background-color: $perf-bar-production;
}
&.staging {
background-color: #291430;
background-color: $perf-bar-staging;
}
&.development {
background-color: #4c1210;
background-color: $perf-bar-development;
}
.wrapper {
width: 800px;
width: 1000px;
margin: 0 auto;
}
// UI Elements
.bucket {
background: #111;
background: $perf-bar-bucket-bg;
display: inline-block;
padding: 4px 6px;
font-family: Consolas, "Liberation Mono", Courier, monospace;
line-height: 1;
color: #ccc;
color: $perf-bar-bucket-color;
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 {
display: none;
......@@ -53,12 +50,14 @@ header.navbar-gitlab.with-peek {
}
strong {
color: #fff;
color: $white-light;
}
table {
color: $black;
strong {
color: #000;
color: $black;
}
}
......@@ -90,5 +89,15 @@ header.navbar-gitlab.with-peek {
}
#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
:mirror_max_delay,
:mirror_max_capacity,
:mirror_capacity_threshold,
:authorized_keys_enabled
:authorized_keys_enabled,
:slack_app_enabled,
:slack_app_id,
:slack_app_secret,
:slack_app_verification_token
]
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
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
delegate :slack_app_id, to: :current_application_settings
def ldap_enabled?
Gitlab::LDAP::Config.enabled?
end
......@@ -72,5 +74,9 @@ module AuthHelper
%w(saml cas3).exclude?(provider.to_s)
end
def slack_redirect_uri(project)
slack_auth_project_settings_slack_url(project)
end
extend self
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
prepend ::EE::FormHelper
def form_errors(model)
return unless model.errors.any?
......@@ -28,7 +30,11 @@ module FormHelper
null_user: true,
current_user: true,
project_id: @project.id,
<<<<<<< HEAD
field_name: 'issue[assignee_ids][]',
=======
field_name: "issue[assignee_ids][]",
>>>>>>> upstream/master
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......
......@@ -23,7 +23,6 @@ module NavHelper
def nav_header_class
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
class_name << " with-peek" if peek_enabled?
class_name
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
prepend EE::SearchHelper
def search_autocomplete_opts(term)
return unless current_user
......
......@@ -266,7 +266,11 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48,
user_default_external: false,
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
......
......@@ -109,10 +109,13 @@ module Issuable
def allows_multiple_assignees?
false
end
<<<<<<< HEAD
def has_multiple_assignees?
assignees.count > 1
end
=======
>>>>>>> upstream/master
end
module ClassMethods
......
......@@ -5,6 +5,11 @@ module EE
author.support_bot? || super
end
# override
def allows_multiple_assignees?
project.feature_available?(:multiple_issue_assignees)
end
# override
def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support
......
......@@ -29,6 +29,26 @@ module EE
validates :plan, inclusion: { in: EE_PLANS.keys }, allow_blank: true
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
# for a given Namespace plan. This method should consider ancestor groups
# being licensed.
......
......@@ -429,6 +429,21 @@ module EE
end
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
def licensed_feature_available?(feature)
......
......@@ -9,5 +9,9 @@ module Geo
belongs_to :repository_deleted_event,
class_name: 'Geo::RepositoryDeletedEvent',
foreign_key: :repository_deleted_event_id
belongs_to :repository_renamed_event,
class_name: 'Geo::RepositoryRenamedEvent',
foreign_key: :repository_renamed_event_id
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
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
MERGE_REQUEST_SQUASH_FEATURE = 'GitLab_MergeRequestSquash'.freeze
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze
......@@ -48,6 +49,7 @@ class License < ActiveRecord::Base
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
merge_request_rebase: MERGE_REQUEST_REBASE_FEATURE,
merge_request_squash: MERGE_REQUEST_SQUASH_FEATURE,
multiple_issue_assignees: MULTIPLE_ISSUE_ASSIGNEES_FEATURE,
push_rules: PUSH_RULES_FEATURE
}.freeze
......@@ -70,6 +72,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ RELATED_ISSUES_FEATURE => 1 }
].freeze
......@@ -113,6 +116,7 @@ class License < ActiveRecord::Base
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 },
{ MERGE_REQUEST_REBASE_FEATURE => 1 },
{ MERGE_REQUEST_SQUASH_FEATURE => 1 },
{ MULTIPLE_ISSUE_ASSIGNEES_FEATURE => 1 },
{ OBJECT_STORAGE_FEATURE => 1 },
{ PUSH_RULES_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 }
......
......@@ -144,6 +144,7 @@ class Namespace < ActiveRecord::Base
# So we basically we mute exceptions in next actions
begin
send_update_instructions
true
rescue
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
......
......@@ -89,6 +89,7 @@ class Project < ActiveRecord::Base
# Project services
has_one :campfire_service
has_one :drone_ci_service
has_one :gitlab_slack_application_service
has_one :emails_on_push_service
has_one :pipelines_email_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
prometheus
pushover
redmine
slack_slash_commands
slack
teamcity
microsoft_teams
......@@ -254,6 +253,14 @@ class Service < ActiveRecord::Base
service_names += %w[mock_ci mock_deployment mock_monitoring]
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)
end
......@@ -264,6 +271,10 @@ class Service < ActiveRecord::Base
service
end
def self.show_gitlab_slack_application?
(Gitlab.com? && current_application_settings.slack_app_enabled) || Rails.env.development?
end
private
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
class RepositoryDeletedEventStore
attr_reader :project, :repo_path, :wiki_path
class RepositoryDeletedEventStore < EventStore
self.event_type = :repository_deleted_event
def initialize(project, repo_path:, wiki_path:)
@project = project
@repo_path = repo_path
@wiki_path = wiki_path
end
def create
return unless Gitlab::Geo.primary?
private
Geo::EventLog.transaction do
event_log = Geo::EventLog.new
deleted_event = Geo::RepositoryDeletedEvent.new(
project: project,
repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path,
deleted_path: repo_path,
deleted_wiki_path: wiki_path,
deleted_project_name: project.name)
event_log.repository_deleted_event = deleted_event
event_log.save
end
def build_event
Geo::RepositoryDeletedEvent.new(
project: project,
repository_storage_name: project.repository.storage,
repository_storage_path: project.repository_storage_path,
deleted_path: params.fetch(:repo_path),
deleted_wiki_path: params.fetch(:wiki_path),
deleted_project_name: project.name)
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
class RepositoryUpdatedEventStore
attr_reader :project, :source, :refs, :changes
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
class RepositoryUpdatedEventStore < EventStore
self.event_type = :repository_updated_event
private
......@@ -35,6 +16,18 @@ module Geo
)
end
def refs
params.fetch(:refs, [])
end
def changes
params.fetch(:changes, [])
end
def source
params.fetch(:source, Geo::RepositoryUpdatedEvent::REPOSITORY)
end
def ref
refs.first if refs.length == 1
end
......@@ -54,9 +47,5 @@ module Geo
def push_remove_branch?
changes.any? { |change| Gitlab::Git.branch_ref?(change[:ref]) && Gitlab::Git.blank_ref?(change[:after]) }
end
def log(message)
Rails.logger.info("#{self.class.name}: #{message} for project #{project.path_with_namespace} (#{project.id})")
end
end
end
......@@ -24,6 +24,10 @@ module Issues
def filter_assignee(issuable)
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) }
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
include Gitlab::ShellAdapter
TransferError = Class.new(StandardError)
prepend ::EE::Projects::TransferService
def execute(new_namespace)
@new_namespace = new_namespace
......
module QuickActions
class InterpretService < BaseService
include Gitlab::QuickActions::Dsl
prepend EE::QuickActions::InterpretService
attr_reader :issuable
......@@ -136,6 +137,7 @@ module QuickActions
parse_params do |unassign_param|
# 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?
<<<<<<< HEAD
end
command :unassign do |users = nil|
@updates[:assignee_ids] =
......@@ -170,21 +172,16 @@ module QuickActions
else
[users.last.id]
end
=======
>>>>>>> upstream/master
end
desc 'Change assignee(s)'
explanation do
'Change assignee(s)'
end
params '@user1 @user2'
condition do
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)
command :unassign do |users = nil|
@updates[:assignee_ids] =
if users&.any?
issuable.assignees.pluck(:id) - users.map(&:id)
else
[]
end
end
desc 'Set milestone'
......@@ -486,34 +483,6 @@ module QuickActions
@updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
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'
explanation do |target_list_name|
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 @@
if you have configured your OpenSSH server to use the
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')
- 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?
%fieldset
......
......@@ -27,10 +27,11 @@
%td.shortcut
.key f
%td Focus Filter
%tr
%td.shortcut
.key p b
%td Show/hide the Performance Bar
- if performance_bar_enabled?
%tr
%td.shortcut
.key p b
%td Show/hide the Performance Bar
%tr
%td.shortcut
.key ?
......
......@@ -30,7 +30,7 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
= 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?
= stylesheet_link_tag "new_nav", media: "all"
......@@ -44,7 +44,7 @@
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= 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)
= yield :page_specific_javascripts
......
......@@ -3,7 +3,6 @@
= render "layouts/head"
%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 'peek/bar'
- if show_new_nav?
= render "layouts/header/new"
- else
......@@ -11,3 +10,5 @@
= render 'layouts/page', sidebar: sidebar, nav: nav
= 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
%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}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 }
.modal-dialog
#modal-peek-pg-queries-content.modal-content
.modal-dialog.modal-full
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%button.close.btn.btn-link.btn-sm{ type: 'button', data: { dismiss: 'modal' } } X
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
......@@ -19,10 +19,11 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.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-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
Select assignee(s)
= dropdown_options[:title]
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
= dropdown_title("Assign to")
......
......@@ -23,7 +23,7 @@
- disabled_class = 'disabled'
- 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)
%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 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- 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'] if dropdown_options[:data][:'max-select']
>>>>>>> upstream/master
- options[:data].merge!(data)
= 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
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
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 << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
......
......@@ -438,6 +438,11 @@ constraints(ProjectUrlConstrainer.new) do
resource :members, only: [:show]
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
resource :slack, only: [:destroy] do
get :slack_auth
end
resource :repository, only: [:show], controller: :repository
end
......
......@@ -74,7 +74,7 @@ var config = {
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
peek: './peek.js',
performance_bar: './performance_bar.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
t.boolean "authorized_keys_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false
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
create_table "approvals", force: :cascade do |t|
......@@ -584,6 +588,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do
t.datetime "created_at", null: false
t.integer "repository_updated_event_id", limit: 8
t.integer "repository_deleted_event_id", limit: 8
t.integer "repository_renamed_event_id", limit: 8
end
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
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|
t.datetime "created_at", null: false
t.integer "branches_affected", null: false
......@@ -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", ["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|
t.string "title"
t.text "content"
......@@ -1846,7 +1878,9 @@ ActiveRecord::Schema.define(version: 20170703102400) do
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 "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_repository_renamed_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 "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
......@@ -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 "services", "projects", name: "fk_71cce407f9", 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 "system_note_metadata", "notes", name: "fk_d83a918cb1", 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).
In many databases documentation you will see `primary` being references as `master`
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
The following guide assumes that:
......@@ -73,6 +78,8 @@ The following guide assumes that:
postgresql['listen_address'] = "1.2.3.4"
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']
# 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['wal_keep_segments'] = 10
```
......@@ -118,6 +125,7 @@ The following guide assumes that:
postgresql['listen_address'] = "10.1.5.3"
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['max_replication_slots'] = 1 # Number of Geo secondary nodes
# postgresql['max_wal_senders'] = 10
# postgresql['wal_keep_segments'] = 10
```
......@@ -138,6 +146,8 @@ The following guide assumes that:
1. Check to make sure your firewall rules are set so that the secondary nodes
can access port 5432 on the primary node.
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
`netstat -plnt` to make sure that PostgreSQL is listening to the server's
public IP.
......@@ -196,16 +206,25 @@ data before running `pg_basebackup`.
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:
```
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
parameters with `--help`. When prompted, enter the password you set up for
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.
### Next steps
......
......@@ -68,10 +68,14 @@ The following guide assumes that:
max_wal_senders = 5
min_wal_size = 80MB
max_wal_size = 1GB
max_replicaton_slots = 1 # Number of Geo secondary nodes
wal_keep_segments = 10
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`.
You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
......@@ -102,6 +106,22 @@ The following guide assumes that:
```
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
`netstat -plnt` to make sure that PostgreSQL is listening to the server's
public IP.
......
......@@ -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
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
your browser. We perform the following health checks on each secondary node
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
end
# EE-only
%w(license).each do |f|
%w(test_license).each do |f|
require Rails.root.join('spec', 'support', f)
end
......
......@@ -770,5 +770,17 @@ module API
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
......@@ -82,7 +82,7 @@ module Gitlab
end
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
when 'repository'
......
......@@ -28,10 +28,16 @@ module Gitlab
# @return [Integer] id of last replicated event
def self.last_processed
last = ::Geo::EventLogState.last_processed.try(:id)
last = ::Geo::EventLogState.last_processed&.id
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
# private methods
......
module Gitlab
module PerformanceBar
def self.enabled?
Feature.enabled?('gitlab_performance_bar')
Rails.env.development? || Feature.enabled?('gitlab_performance_bar')
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
]
end.sort_by{ |a,b,c,d,e,f| -f }
output = ''
per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort|
output = "<div class='modal-dialog modal-full'><div class='modal-content'>"
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'>"
show_src = file_sort > min
......@@ -86,11 +91,32 @@ module Peek
output << "</div></div>" # .data then .peek-rblineprof-file
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
ret
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
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
type 'HipchatService'
token 'test_token'
end
factory :gitlab_slack_application_service do
project factory: :empty_project
type 'GitlabSlackApplicationService'
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
before do
Timecop.freeze
stub_licensed_features(multiple_issue_assignees: false)
project.team << [user, :master]
project.team.add_developer(user2)
gitlab_sign_in(user)
......@@ -117,26 +117,6 @@ describe 'Issue Boards', feature: true, js: true do
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)
......@@ -150,8 +130,6 @@ describe 'Issue Boards', feature: true, js: true do
click_link 'Unassigned'
end
find('.dropdown-menu-toggle').click
wait_for_requests
expect(page).to have_content('No assignee')
......
require 'rails_helper'
describe 'New/edit issue (EE)', :feature, :js do
describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper
include FormHelper
......@@ -16,6 +16,8 @@ describe 'New/edit issue (EE)', :feature, :js do
before do
project.team << [user, :master]
project.team << [user2, :master]
stub_licensed_features(multiple_issue_assignees: true)
gitlab_sign_in(user)
end
......@@ -24,15 +26,15 @@ describe 'New/edit issue (EE)', :feature, :js do
visit new_project_issue_path(project)
end
describe 'shorten users API pagination limit (CE)' do
describe 'shorten users API pagination limit' do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called.
# 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.
original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options)
allow_any_instance_of(FormHelper).to receive(:issue_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|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
......@@ -96,5 +98,174 @@ describe 'New/edit issue (EE)', :feature, :js do
expect(find('a', text: 'Assign to me')).to be_visible
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
......@@ -13,6 +13,8 @@ describe 'New/edit issue', :feature, :js do
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.team << [user, :master]
project.team << [user2, :master]
gitlab_sign_in(user)
......@@ -23,15 +25,20 @@ describe 'New/edit issue', :feature, :js do
visit new_project_issue_path(project)
end
xdescribe 'shorten users API pagination limit (CE)' do
describe 'shorten users API pagination limit' do
before do
# Using `allow_any_instance_of`/`and_wrap_original`, `original` would
# somehow refer to the very block we defined to _wrap_ that method, instead of
# the original method, resulting in infinite recurison when called.
# 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.
<<<<<<< HEAD
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|
=======
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[:data][:per_page] = 2
......@@ -63,7 +70,7 @@ describe 'New/edit issue', :feature, :js do
end
end
xdescribe 'single assignee (CE)' do
describe 'single assignee' do
before do
click_button 'Unassigned'
......@@ -122,11 +129,10 @@ describe 'New/edit issue', :feature, :js do
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)
expect(assignee_ids[0].value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content "#{user2.name} + 1 more"
expect(page).to have_content user.name
end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
......@@ -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)[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"
expect(page).to have_content "Assignee"
end
page.within '.milestone' do
......@@ -173,10 +173,6 @@ describe 'New/edit issue', :feature, :js 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
......@@ -214,18 +210,13 @@ describe 'New/edit issue', :feature, :js do
end
expect(find('.js-assignee-search')).to have_content(user.name)
click_button 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)
expect(find('.js-assignee-search')).to have_content(user2.name)
end
it 'description has autocomplete' do
......
......@@ -40,23 +40,11 @@
"additionalProperties": false
}
},
"assignees": {
"type": "array",
"items": {
"type": ["object", "null"],
"required": [
"id",
"name",
"username",
"avatar_url"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
}
}
"assignee": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
},
"assignees": {
"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 @@
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"weight": { "type": ["integer", "null"] }
"web_url": { "type": "uri" }
},
"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"
"web_url"
],
"additionalProperties": false
}
......
......@@ -15,6 +15,9 @@ import '~/filtered_search/filtered_search_token_keys_issues_ee';
let tokenKeys;
beforeEach(() => {
gl.FilteredSearchTokenKeysIssuesEE.init({
multipleAssignees: true,
});
tokenKeys = gl.FilteredSearchTokenKeysIssuesEE.get();
});
......
......@@ -2,10 +2,6 @@ require 'spec_helper'
describe Gitlab::Geo::LogCursor::Daemon, lib: true do
describe '#run!' do
before do
allow(subject).to receive(:exit?) { true }
end
it 'traps signals' do
allow(subject).to receive(:exit?) { true }
expect(subject).to receive(:trap_signals)
......@@ -17,10 +13,44 @@ describe Gitlab::Geo::LogCursor::Daemon, lib: true do
subject { described_class.new(full_scan: true) }
it 'executes a full-scan' do
allow(subject).to receive(:exit?) { true }
expect(subject).to receive(:full_scan!)
subject.run!
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
......@@ -2,20 +2,27 @@ require 'spec_helper'
describe Gitlab::Geo::LogCursor::Events, lib: true 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
allow(described_class).to receive(:last_processed) { -1 }
context 'when no event_log_state exist' do
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
it 'yields a group of events' do
expect { |b| described_class.fetch_in_batches(&b) }.to yield_with_args([event_log])
end
context 'when there is already an event_log_state' do
let!(:event_log_state) { create(:geo_event_log_state, event_id: event_log_1.id - 1) }
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
expect(described_class).to receive(:save_processed)
it 'saves last event as last processed after yielding' do
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
it 'skips execution if cannot achieve a lease' do
......@@ -26,15 +33,15 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do
end
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(Geo::EventLogState.last.event_id).to eq(1)
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)
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)
end
end
......@@ -52,6 +59,11 @@ describe Gitlab::Geo::LogCursor::Events, lib: true do
it 'returns last event id' do
expect(described_class.last_processed).to eq(event_log.id)
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
context 'when there is already an event_log_state' do
......
......@@ -190,6 +190,7 @@ project:
- pipelines_email_service
- mattermost_slash_commands_service
- slack_slash_commands_service
- gitlab_slack_application_service
- irker_service
- pivotaltracker_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'
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
[
{ license: true, database: 5, expected: 5 },
......
......@@ -42,6 +42,34 @@ describe Namespace, models: true do
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
let(:plan_license) { Namespace::BRONZE_PLAN }
let(:group) { create(:group, plan: plan_license) }
......
......@@ -639,4 +639,37 @@ describe Project, models: true do
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
......@@ -3,5 +3,6 @@ require 'spec_helper'
RSpec.describe Geo::EventLog, type: :model 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_renamed_event).class_name('Geo::RepositoryRenamedEvent').with_foreign_key('repository_renamed_event_id') }
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
end
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'))
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 })
.to include({ 'GitLab_DeployBoard' => 1, 'GitLab_FileLocks' => 1 } )
end
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
project.team << [guest, :guest]
end
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe "GET /issues" do
context "when unauthenticated" do
it "returns authentication error" do
......@@ -691,7 +695,6 @@ describe API::Issues do
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
expect(json_response['weight']).to be_nil
end
it "returns a project issue by internal id" do
......@@ -773,6 +776,17 @@ describe API::Issues do
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
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
......@@ -783,7 +797,6 @@ describe API::Issues do
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
......@@ -1113,6 +1126,17 @@ describe API::Issues do
expect(json_response['assignees'].first['name']).to eq(user2.name)
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
describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
......@@ -1218,52 +1242,6 @@ describe API::Issues do
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
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
......
......@@ -11,6 +11,8 @@ describe API::Milestones do
before do
project.team << [user, :developer]
stub_licensed_features(issue_weights: false)
end
describe 'GET /projects/:id/milestones' do
......
......@@ -174,4 +174,21 @@ describe API::Services do
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
......@@ -10,32 +10,22 @@ describe Projects::DestroyService, services: true do
let!(:wiki_path) { project.path_with_namespace + '.wiki' }
let!(:storage_name) { project.repository_storage }
let!(:storage_path) { project.repository_storage_path }
let!(:geo_node) { create(:geo_node, :primary, :current) }
subject { described_class.new(project, user, {}) }
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: :any, tags: [])
end
context 'Geo primary' do
it 'logs the event' do
# Run sidekiq immediatly to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
event = Geo::RepositoryDeletedEvent.first
context 'when running on a primary node' do
let!(:geo_node) { create(:geo_node, :primary, :current) }
expect(Geo::EventLog.count).to eq(1)
expect(Geo::RepositoryDeletedEvent.count).to eq(1)
expect(event.project_id).to eq(project_id)
expect(event.deleted_path).to eq(project_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)
it 'logs an event to the Geo event log' do
# Run Sidekiq immediately to check that renamed repository will be removed
Sidekiq::Testing.inline! do
expect { subject.execute }.to change(Geo::RepositoryDeletedEvent, :count).by(1)
end
end
end
def destroy_project(project, user, params = {})
described_class.new(project, user, params).execute
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
end
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)
end
......@@ -80,7 +80,7 @@ describe Projects::TransferService, services: true do
end
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
end
......
......@@ -11,6 +11,8 @@ describe QuickActions::InterpretService, services: true do
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do
stub_licensed_features(multiple_issue_assignees: false)
project.team << [developer, :developer]
end
......@@ -399,21 +401,15 @@ describe QuickActions::InterpretService, services: true do
let(:content) { "/assign @#{developer.username}" }
context 'Issue' do
it 'fetches assignees and populates them if content contains /assign' do
user = create(:user)
issue.assignees << user
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, user.id])
expect(updates[:assignee_ids]).to match_array([developer.id])
end
end
context 'Merge Request' do
it 'fetches assignee and populates assignee_id if content contains /assign' do
user = create(:user)
merge_request.update(assignee: user)
it 'fetches assignee and populates assignee_ids if content contains /assign' do
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_ids: [developer.id])
......@@ -432,7 +428,7 @@ describe QuickActions::InterpretService, services: true do
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])
expect(updates[:assignee_ids]).to match_array([developer.id])
end
end
......@@ -459,46 +455,11 @@ describe QuickActions::InterpretService, services: true 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
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)
it 'populates assignee_ids: [] if content contains /unassign' do
issue.update(assignee_ids: [developer.id])
_, updates = service.execute(content, issue)
expect(updates).to eq(assignee_ids: [user.id])
end
expect(updates).to eq(assignee_ids: [])
end
end
......@@ -508,6 +469,7 @@ describe QuickActions::InterpretService, services: true do
_, updates = service.execute(content, merge_request)
expect(updates).to eq(assignee_ids: [])
<<<<<<< HEAD
end
end
end
......@@ -524,6 +486,8 @@ describe QuickActions::InterpretService, services: true do
_, updates = service.execute("/reassign @#{user.username}", issue)
expect(updates).to eq(assignee_ids: [user.id])
=======
>>>>>>> upstream/master
end
end
end
......@@ -1016,7 +980,7 @@ describe QuickActions::InterpretService, services: true do
it 'includes current assignee reference' do
_, explanations = service.explain(content, issue)
expect(explanations).to eq(["Removes assignee #{developer.to_reference}"])
expect(explanations).to eq(["Removes assignee @#{developer.username}."])
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