Commit 068a6599 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 19703-direct-link-pipelines

* master: (30 commits)
  Add GitLab host to 2FA QR and manual info
  Fix broken test
  Fix rubocop
  Fix specs in Ruby 2.1
  Clearer comment as to why the procedure is needed
  Ensure issuable state changes only fire webhooks once
  Improve performance on RemoveDuplicatesFromRoutes migration
  Fix the AddNameIndexToNamespace migration to be reversible
  Use optimized query to fill the routes table when running PostgreSQL
  Don't use the Route model in migrations
  Added KaTeX license and procedure to build it for Gitlab
  [ci skip] UX Guide: add guidance on cursor usage
  Changes after review
  Add missing group policy spec
  Limit description container for mrs while viewing side by side diff
  Refactor Namespace#parents method
  Change SlackService to SlackNotificationsService
  Made Ci::Builds to have same ref as Ci::Pipeline in dev fixtures
  Mattermost Notifications Service
  Replace static fixture for abuse_reports_spec (!7644)
  ...
parents 637f5c15 34875519
...@@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false ...@@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API # API
gem 'grape', '~> 0.15.0' gem 'grape', '~> 0.18.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
......
...@@ -284,15 +284,15 @@ GEM ...@@ -284,15 +284,15 @@ GEM
json json
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
grape (0.15.0) grape (0.18.0)
activesupport activesupport
builder builder
hashie (>= 2.1.0) hashie (>= 2.1.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
mustermann-grape (~> 0.4.0)
rack (>= 1.3.0) rack (>= 1.3.0)
rack-accept rack-accept
rack-mount
virtus (>= 1.0.0) virtus (>= 1.0.0)
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
...@@ -400,6 +400,10 @@ GEM ...@@ -400,6 +400,10 @@ GEM
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.5.5) multi_xml (0.5.5)
multipart-post (2.0.0) multipart-post (2.0.0)
mustermann (0.4.0)
tool (~> 0.2)
mustermann-grape (0.4.0)
mustermann (= 0.4.0)
mysql2 (0.3.20) mysql2 (0.3.20)
net-ldap (0.12.1) net-ldap (0.12.1)
net-ssh (3.0.1) net-ssh (3.0.1)
...@@ -505,14 +509,12 @@ GEM ...@@ -505,14 +509,12 @@ GEM
pry-rails (0.3.4) pry-rails (0.3.4)
pry (>= 0.9.10) pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.4) rack (1.6.5)
rack-accept (0.4.5) rack-accept (0.4.5)
rack (>= 0.4) rack (>= 0.4)
rack-attack (4.4.1) rack-attack (4.4.1)
rack rack
rack-cors (0.4.0) rack-cors (0.4.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
rack-oauth2 (1.2.3) rack-oauth2 (1.2.3)
activesupport (>= 2.3) activesupport (>= 2.3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
...@@ -743,6 +745,7 @@ GEM ...@@ -743,6 +745,7 @@ GEM
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tool (0.2.3)
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1) nokogiri (~> 1.6.1)
...@@ -861,7 +864,7 @@ DEPENDENCIES ...@@ -861,7 +864,7 @@ DEPENDENCIES
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0) gon (~> 6.1.0)
grape (~> 0.15.0) grape (~> 0.18.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2) haml_lint (~> 0.18.2)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
......
...@@ -26,6 +26,10 @@ body { ...@@ -26,6 +26,10 @@ body {
.container-limited { .container-limited {
max-width: $fixed-layout-width; max-width: $fixed-layout-width;
&.limit-container-width {
max-width: $limited-layout-width;
}
} }
......
...@@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff; ...@@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958; $error-exclamation-point: #e62958;
$border-radius-default: 2px; $border-radius-default: 2px;
$settings-icon-size: 18px; $settings-icon-size: 18px;
......
// Limit MR description for side-by-side diff view
.limit-container-width {
.detail-page-header {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.issuable-details {
.detail-page-description,
.mr-source-target,
.mr-state-widget,
.merge-manually {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
.container-fluid {
padding-left: 0;
padding-right: 0;
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.diffs {
.mr-version-controls,
.files-changed {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
...@@ -9,7 +56,6 @@ ...@@ -9,7 +56,6 @@
.description img:not(.emoji) { .description img:not(.emoji) {
border: 1px solid $white-normal; border: 1px solid $white-normal;
padding: 5px; padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
} }
} }
......
...@@ -383,10 +383,6 @@ ul.notes { ...@@ -383,10 +383,6 @@ ul.notes {
.note-action-button { .note-action-button {
margin-left: 10px; margin-left: 10px;
} }
@media (min-width: $screen-sm-min) {
position: relative;
}
} }
.discussion-actions { .discussion-actions {
......
...@@ -616,14 +616,10 @@ ...@@ -616,14 +616,10 @@
li { li {
padding-top: 2px; padding-top: 2px;
margin: 0 5px; margin: 0 5px;
} padding-left: 0;
padding-bottom: 0;
li:first-child { margin-bottom: 0;
padding-top: 6px; line-height: 1.2;
}
li:last-child {
padding-bottom: 6px;
} }
} }
} }
......
...@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end end
@qr_code = build_qr_code @qr_code = build_qr_code
@account_string = account_string
setup_u2f_registration setup_u2f_registration
end end
...@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private private
def build_qr_code def build_qr_code
issuer = "#{issuer_host} | #{current_user.email}" uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end end
def account_string
"#{issuer_host}:#{current_user.email}"
end
def issuer_host def issuer_host
Gitlab.config.gitlab.host Gitlab.config.gitlab.host
end end
......
...@@ -12,11 +12,18 @@ module GroupsHelper ...@@ -12,11 +12,18 @@ module GroupsHelper
end end
def group_title(group, name = nil, url = nil) def group_title(group, name = nil, url = nil)
full_title = link_to(simple_sanitize(group.name), group_path(group)) full_title = ''
group.parents.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
full_title += link_to(simple_sanitize(group.name), group_path(group))
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
content_tag :span do content_tag :span do
full_title full_title.html_safe
end end
end end
......
...@@ -52,7 +52,7 @@ module ProjectsHelper ...@@ -52,7 +52,7 @@ module ProjectsHelper
def project_title(project) def project_title(project)
namespace_link = namespace_link =
if project.group if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group)) group_title(project.group)
else else
owner = project.namespace.owner owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
...@@ -390,7 +390,7 @@ module ProjectsHelper ...@@ -390,7 +390,7 @@ module ProjectsHelper
"success" "success"
end end
end end
def readme_cache_key def readme_cache_key
sha = @project.commit.try(:sha) || 'nil' sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-') [@project.path_with_namespace, sha, "readme"].join('-')
......
...@@ -83,7 +83,7 @@ class Group < Namespace ...@@ -83,7 +83,7 @@ class Group < Namespace
end end
def human_name def human_name
name full_name
end end
def visibility_level_field def visibility_level_field
......
...@@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base ...@@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base
end end
end end
def full_name
@full_name ||=
if parent
parent.full_name + ' / ' + name
else
name
end
end
def parents
@parents ||= parent ? parent.parents + [parent] : []
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -95,7 +95,8 @@ class Project < ActiveRecord::Base ...@@ -95,7 +95,8 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :slack_service, dependent: :destroy has_one :mattermost_notification_service, dependent: :destroy
has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy
......
require 'slack-notifier' require 'slack-notifier'
class SlackService module ChatMessage
class BaseMessage class BaseMessage
def initialize(params) def initialize(params)
raise NotImplementedError raise NotImplementedError
......
class SlackService module ChatMessage
class BuildMessage < BaseMessage class BuildMessage < BaseMessage
attr_reader :sha attr_reader :sha
attr_reader :ref_type attr_reader :ref_type
......
class SlackService module ChatMessage
class IssueMessage < BaseMessage class IssueMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :title attr_reader :title
......
class SlackService module ChatMessage
class MergeMessage < BaseMessage class MergeMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :project_name attr_reader :project_name
......
class SlackService module ChatMessage
class NoteMessage < BaseMessage class NoteMessage < BaseMessage
attr_reader :message attr_reader :message
attr_reader :user_name attr_reader :user_name
......
class SlackService module ChatMessage
class PipelineMessage < BaseMessage class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url, attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id :user_name, :duration, :pipeline_id
......
class SlackService module ChatMessage
class PushMessage < BaseMessage class PushMessage < BaseMessage
attr_reader :after attr_reader :after
attr_reader :before attr_reader :before
......
class SlackService module ChatMessage
class WikiPageMessage < BaseMessage class WikiPageMessage < BaseMessage
attr_reader :user_name attr_reader :user_name
attr_reader :title attr_reader :title
......
class SlackService < Service # Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
class ChatNotificationService < Service
include ChatMessage
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated? validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties def initialize_properties
...@@ -14,35 +21,8 @@ class SlackService < Service ...@@ -14,35 +21,8 @@ class SlackService < Service
end end
end end
def title def can_test?
'Slack' valid?
end
def description
'A team communication tool for the 21st century'
end
def to_param
'slack'
end
def help
'This service sends notifications to your Slack channel.<br/>
To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
and enter the Webhook URL below.'
end
def fields
default_fields =
[
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
default_fields + build_event_channels
end end
def supported_events def supported_events
...@@ -67,21 +47,16 @@ class SlackService < Service ...@@ -67,21 +47,16 @@ class SlackService < Service
message = get_message(object_kind, data) message = get_message(object_kind, data)
if message return false unless message
opt = {}
event_channel = get_channel_field(object_kind) || channel
opt[:channel] = event_channel if event_channel opt = {}
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt) opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true true
else
false
end
end end
def event_channel_names def event_channel_names
...@@ -96,6 +71,10 @@ class SlackService < Service ...@@ -96,6 +71,10 @@ class SlackService < Service
fields.reject { |field| field[:name].end_with?('channel') } fields.reject { |field| field[:name].end_with?('channel') }
end end
def default_channel
raise NotImplementedError
end
private private
def get_message(object_kind, data) def get_message(object_kind, data)
...@@ -124,7 +103,7 @@ class SlackService < Service ...@@ -124,7 +103,7 @@ class SlackService < Service
def build_event_channels def build_event_channels
supported_events.reduce([]) do |channels, event| supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" } channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
end end
end end
...@@ -166,11 +145,3 @@ class SlackService < Service ...@@ -166,11 +145,3 @@ class SlackService < Service
end end
end end
end end
require "slack_service/issue_message"
require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
require "slack_service/pipeline_message"
require "slack_service/wiki_page_message"
# Base class for Chat services # Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from. # This class is not meant to be used directly, but only to inherit from.
class ChatService < Service class ChatService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
......
class MattermostNotificationService < ChatNotificationService
def title
'Mattermost notifications'
end
def description
'Receive event notifications in Mattermost'
end
def to_param
'mattermost_notification'
end
def help
'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service:
<ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel
"#town-square"
end
end
class SlackNotificationService < ChatNotificationService
def title
'Slack notifications'
end
def description
'Receive event notifications in Slack'
end
def to_param
'slack_notification'
end
def help
'This service sends notifications about projects events to Slack channels.<br />
To setup this service:
<ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li>
<li>Select events below to enable notifications. The channel and username are optional. </li>
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel
"#general"
end
end
...@@ -220,7 +220,8 @@ class Service < ActiveRecord::Base ...@@ -220,7 +220,8 @@ class Service < ActiveRecord::Base
pivotaltracker pivotaltracker
pushover pushover
redmine redmine
slack mattermost_notification
slack_notification
teamcity teamcity
] ]
end end
......
...@@ -184,7 +184,8 @@ class IssuableBaseService < BaseService ...@@ -184,7 +184,8 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a old_mentioned_users = issuable.mentioned_users.to_a
params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids) label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
if params.present? && update_issuable(issuable, params) if params.present? && update_issuable(issuable, params)
# We do not touch as it will affect a update on updated_at field # We do not touch as it will affect a update on updated_at field
...@@ -201,6 +202,10 @@ class IssuableBaseService < BaseService ...@@ -201,6 +202,10 @@ class IssuableBaseService < BaseService
issuable issuable
end end
def labels_changing?(old_label_ids, new_label_ids)
old_label_ids.sort != new_label_ids.sort
end
def change_state(issuable) def change_state(issuable)
case params.delete(:state_event) case params.delete(:state_event)
when 'reopen' when 'reopen'
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to [:admin, group], class: 'group-name' do = link_to [:admin, group], class: 'group-name' do
= group.name = group.full_name
- if group.description.present? - if group.description.present?
.description .description
......
- page_title @group.name, "Groups" - page_title @group.name, "Groups"
%h3.page-title %h3.page-title
Group: #{@group.name} Group: #{@group.full_name}
= link_to admin_group_edit_path(@group), class: "btn pull-right" do = link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
......
...@@ -3,10 +3,9 @@ ...@@ -3,10 +3,9 @@
- subject = local_assigns.fetch(:subject) - subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user) - status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status}" - klass = "ci-status-icon ci-status-icon-#{status}"
- tooltip_title = "#{subject.name} - #{status.label}"
- if status.has_details? - if status.has_details?
= link_to status.details_path, data: { toggle: 'tooltip', title: tooltip_title } do = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do
%span{ class: klass }= custom_icon(status.icon) %span{ class: klass }= custom_icon(status.icon)
.ci-status-text= subject.name .ci-status-text= subject.name
- else - else
...@@ -15,6 +14,6 @@ ...@@ -15,6 +14,6 @@
- if status.has_action? - if status.has_action?
= link_to status.action_path, method: status.action_method, = link_to status.action_path, method: status.action_method,
title: tooltip_title, class: 'ci-action-icon-container' do title: status.action_title, class: 'ci-action-icon-container' do
%i.ci-action-icon-wrapper %i.ci-action-icon-wrapper
= icon(status.action_icon, class: status.action_class) = icon(status.action_icon, class: status.action_class)
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone. To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0 %p.prepend-top-0.append-bottom-0
Account: Account:
= current_user.email = @account_string
%p.prepend-top-0.append-bottom-0 %p.prepend-top-0.append-bottom-0
Key: Key:
= current_user.otp_secret.scan(/.{4}/).join(' ') = current_user.otp_secret.scan(/.{4}/).join(' ')
......
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
= ci_icon_for_status('play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
.ci-status-text= subject.name
- else
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
- if subject.target_url
= link_to subject.target_url do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
- else
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
- @content_class = "limit-container-width"
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description - page_description @issue.description
- page_card_attributes @issue.card_attributes - page_card_attributes @issue.card_attributes
......
- @content_class = "limit-container-width"
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description - page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes - page_card_attributes @merge_request.card_attributes
...@@ -41,7 +42,7 @@ ...@@ -41,7 +42,7 @@
= render "projects/merge_requests/widget/show.html.haml" = render "projects/merge_requests/widget/show.html.haml"
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default.append-bottom-default .merge-manually.light.prepend-top-default.append-bottom-default
You can also accept this merge request manually using the You can also accept this merge request manually using the
= succeed '.' do = succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs" = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title .title
= link_to group, class: 'group-name' do = link_to group, class: 'group-name' do
= group.name = group.full_name
- if group_member - if group_member
as as
......
---
title: Ensure issuable state changes only fire webhooks once
merge_request:
author:
---
title: Replace static fixture for abuse_reports_spec
merge_request: 7644
author: winniehell
---
title: Add GitLab host to 2FA QR code and manual info
merge_request: 6941
author:
---
title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
merge_request:
author: twonegatives
---
title: 'Gem update: Update grape to 0.18.0'
merge_request:
author: Robert Schilling
---
title: Create mattermost service
merge_request:
author:
...@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines ...@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
def job_attributes(pipeline, opts) def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]), { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline, ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now created_at: Time.now, updated_at: Time.now
}.merge(opts) }.merge(opts)
end end
......
# rubocop:disable all # rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change def change
SlackService.all.each do |slack_service| SlackNotificationService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token'] token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain'] subdomain = slack_service.properties['subdomain']
......
...@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration ...@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
end end
def down def down
Route.delete_all(source_type: 'Namespace') execute("DELETE FROM routes WHERE source_type = 'Namespace'")
end end
end end
...@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration ...@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
DOWNTIME_REASON = 'No new projects should be created during data copy' DOWNTIME_REASON = 'No new projects should be created during data copy'
def up def up
execute <<-EOF if Gitlab::Database.postgresql?
INSERT INTO routes execute <<-EOF
(source_id, source_type, path) INSERT INTO routes (source_id, source_type, path)
(SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
INNER JOIN namespaces ON projects.namespace_id = namespaces.id) FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
EOF ORDER BY namespaces.path, projects.path, projects.id DESC)
EOF
else
execute <<-EOF
INSERT INTO routes (source_id, source_type, path)
(SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path)
FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
EOF
end
end end
def down def down
Route.delete_all(source_type: 'Project') execute("DELETE FROM routes WHERE source_type = 'Project'")
end end
end end
...@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration ...@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row| # We can skip this migration when running a PostgreSQL database because
path = connection.quote(row['path']) # we use an optimized query in the "FillProjectsRoutesTable" migration
execute(%Q{ # to fill these values that avoid duplicate entries in the routes table.
DELETE FROM #{quote_table_name(:routes)} return unless Gitlab::Database.mysql?
WHERE path = #{path}
AND id != ( execute <<-EOF
SELECT id FROM ( DELETE duplicated_rows.*
SELECT max(id) AS id FROM routes AS duplicated_rows
FROM #{quote_table_name(:routes)} INNER JOIN (
WHERE path = #{path} SELECT path, MAX(id) as max_id
) max_ids FROM routes
) GROUP BY path
}) HAVING COUNT(*) > 1
end ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
EOF
end end
def down def down
......
...@@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration ...@@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration
end end
def down def down
if index_exists?(:namespaces, :name) if index_exists?(:namespaces, [:name, :parent_id])
remove_index :namespaces, [:name, :parent_id] remove_index :namespaces, [:name, :parent_id]
end end
end end
......
class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
def up
execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
end
def down
execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161212142807) do ActiveRecord::Schema.define(version: 20161213172958) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do ...@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.text "help_page_text_html" t.text "help_page_text_html"
t.text "shared_runners_text_html" t.text "shared_runners_text_html"
t.text "after_sign_up_text_html" t.text "after_sign_up_text_html"
t.boolean "sidekiq_throttling_enabled", default: false
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false t.integer "housekeeping_gc_period", default: 200, null: false
t.boolean "sidekiq_throttling_enabled", default: false
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true t.boolean "html_emails_enabled", default: true
end end
...@@ -527,6 +527,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do ...@@ -527,6 +527,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.string "type" t.string "type"
t.string "fingerprint" t.string "fingerprint"
t.boolean "public", default: false, null: false t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
end end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
...@@ -739,8 +740,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do ...@@ -739,8 +740,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.integer "visibility_level", default: 20, null: false t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at" t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html" t.text "description_html"
t.boolean "lfs_enabled"
t.integer "parent_id" t.integer "parent_id"
end end
...@@ -1221,8 +1222,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do ...@@ -1221,8 +1222,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.datetime "otp_grace_period_started_at" t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false t.boolean "external", default: false
t.string "organization"
t.string "incoming_email_token" t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated" t.boolean "authorized_projects_populated"
end end
......
...@@ -44,22 +44,30 @@ as appropriate. ...@@ -44,22 +44,30 @@ as appropriate.
## Chained hooks support ## Chained hooks support
> [Introduced][93] in GitLab Shell 4.1.0. > [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project) Hooks can be also placed in `hooks/<hook_name>.d` (global) or
directories supporting chained execution of the hooks. `custom_hooks/<hook_name>.d` (per project) directories supporting chained
execution of the hooks.
To look in a different directory for the global custom hooks (those in
`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
Omnibus installations, this can be set in `gitlab.rb`; and in source
installations, this can be set in `gitlab-shell/config.yml`.
The hooks are searched and executed in this order: The hooks are searched and executed in this order:
1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir 1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>` 1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior) 1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks 1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files) 1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
Files in `.d` directories need to be executable and not match the backup file pattern (`*~`). Files in `.d` directories need to be executable and not match the backup file
pattern (`*~`).
The hooks of the same type are executed in order and execution stops on the first The hooks of the same type are executed in order and execution stops on the
script exiting with non-zero value. first script exiting with a non-zero value.
## Custom error messages ## Custom error messages
......
...@@ -703,9 +703,9 @@ Get Redmine service settings for a project. ...@@ -703,9 +703,9 @@ Get Redmine service settings for a project.
GET /projects/:id/services/redmine GET /projects/:id/services/redmine
``` ```
## Slack ## Slack notifications
A team communication tool for the 21st century Receive event notifications in Slack
### Create/Edit Slack service ### Create/Edit Slack service
...@@ -737,6 +737,40 @@ Get Slack service settings for a project. ...@@ -737,6 +737,40 @@ Get Slack service settings for a project.
GET /projects/:id/services/slack GET /projects/:id/services/slack
``` ```
## Mattermost notifications
Receive event notifications in Mattermost
### Create/Edit Mattermost notifications service
Set Mattermost service for a project.
```
PUT /projects/:id/services/mattermost
```
Parameters:
- `webhook` (**required**) - https://mattermost.example/hooks/1298aff...
- `username` (optional) - username
- `channel` (optional) - #channel
### Delete Mattermost notifications service
Delete Mattermost Notifications service for a project.
```
DELETE /projects/:id/services/mattermost
```
### Get Mattermost notifications service settings
Get Mattermost notifications service settings for a project.
```
GET /projects/:id/services/mattermost
```
## JetBrains TeamCity CI ## JetBrains TeamCity CI
A continuous integration and build server A continuous integration and build server
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
* [Typography](#typography) * [Typography](#typography)
* [Icons](#icons) * [Icons](#icons)
* [Color](#color) * [Color](#color)
* [Cursors](#cursors)
--- ---
...@@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface. ...@@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface.
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage. > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
---
## Cursors
The mouse cursor is key in helping users understand how to interact with elements on the screen.
| | |
| :------: | :------- |
| ![Default cursor](img/cursors-default.png) | Default cursor |
| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons |
| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen |
| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. |
| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. |
| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. |
# Mattermost Notifications Service
## On Mattermost
To enable Mattermost integration you must create an incoming webhook integration:
1. Sign in to your Mattermost instance
1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
1. Choose a display name, description and channel, those can be overridden on GitLab
1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
it on https://mattermost.example/admin_console/integrations/custom.
Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
## On GitLab
After you set up Mattermost, it's time to set up GitLab.
Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
checkbox with the following events that can be triggered:
- Push
- Issue
- Merge request
- Note
- Tag push
- Build
- Wiki page
Bellow each of these event checkboxes, you will have an input field to insert
which Mattermost channel you want to send that event message, with `#town-square`
being the default. The hash sign is optional.
At the end, fill in your Mattermost details:
| Field | Description |
| ----- | ----------- |
| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
![Mattermost configuration](img/mattermost_configuration.png)
...@@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome. ...@@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome.
| JetBrains TeamCity CI | A continuous integration and build server | | JetBrains TeamCity CI | A continuous integration and build server |
| [Kubernetes](kubernetes.md) | A containerized deployment service | | [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| PivotalTracker | Project Management Software (Source Commits Endpoint) | | PivotalTracker | Project Management Software (Source Commits Endpoint) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker | | [Redmine](redmine.md) | Redmine issue tracker |
| [Slack](slack.md) | A team communication tool for the 21st century |
## Services Templates ## Services Templates
......
# Slack Service # Slack Notifications Service
## On Slack ## On Slack
...@@ -15,7 +15,7 @@ Slack: ...@@ -15,7 +15,7 @@ Slack:
After you set up Slack, it's time to set up GitLab. After you set up Slack, it's time to set up GitLab.
Go to your project's **Settings > Services > Slack** and you will see a Go to your project's **Settings > Services > Slack Notifications** and you will see a
checkbox with the following events that can be triggered: checkbox with the following events that can be triggered:
- Push - Push
......
...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee ...@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.0.3 sudo -u git -H git checkout v4.1.0
``` ```
### 6. Update gitlab-workhorse ### 6. Update gitlab-workhorse
......
...@@ -473,7 +473,7 @@ module API ...@@ -473,7 +473,7 @@ module API
desc: 'The description of the tracker' desc: 'The description of the tracker'
} }
], ],
'slack' => [ 'slack-notification' => [
{ {
required: true, required: true,
name: :webhook, name: :webhook,
...@@ -493,6 +493,14 @@ module API ...@@ -493,6 +493,14 @@ module API
desc: 'The channel name' desc: 'The channel name'
} }
], ],
'mattermost-notification' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
}
],
'teamcity' => [ 'teamcity' => [
{ {
required: true, required: true,
......
...@@ -107,7 +107,7 @@ describe 'Commits' do ...@@ -107,7 +107,7 @@ describe 'Commits' do
describe 'Cancel build' do describe 'Cancel build' do
it 'cancels build' do it 'cancels build' do
visit ci_status_path(pipeline) visit ci_status_path(pipeline)
click_on 'Cancel' find('a.btn[title="Cancel"]').click
expect(page).to have_content 'canceled' expect(page).to have_content 'canceled'
end end
end end
......
...@@ -38,8 +38,8 @@ describe "Pipelines", feature: true, js: true do ...@@ -38,8 +38,8 @@ describe "Pipelines", feature: true, js: true do
expect(page).to have_css('#js-tab-pipeline.active') expect(page).to have_css('#js-tab-pipeline.active')
end end
context 'pipeline graph' do describe 'pipeline graph' do
context 'running build' do context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do it 'shows a running icon and a cancel action for the running build' do
page.within('a[data-title="deploy - running"]') do page.within('a[data-title="deploy - running"]') do
expect(page).to have_selector('.ci-status-icon-running') expect(page).to have_selector('.ci-status-icon-running')
...@@ -58,7 +58,7 @@ describe "Pipelines", feature: true, js: true do ...@@ -58,7 +58,7 @@ describe "Pipelines", feature: true, js: true do
end end
end end
context 'success build' do context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successfull build' do it 'shows the success icon and a retry action for the successfull build' do
page.within('a[data-title="build - passed"]') do page.within('a[data-title="build - passed"]') do
expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_selector('.ci-status-icon-success')
...@@ -77,7 +77,7 @@ describe "Pipelines", feature: true, js: true do ...@@ -77,7 +77,7 @@ describe "Pipelines", feature: true, js: true do
end end
end end
context 'failed build' do context 'when pipeline has failed builds' do
it 'shows the failed icon and a retry action for the failed build' do it 'shows the failed icon and a retry action for the failed build' do
page.within('a[data-title="test - failed"]') do page.within('a[data-title="test - failed"]') do
expect(page).to have_selector('.ci-status-icon-failed') expect(page).to have_selector('.ci-status-icon-failed')
...@@ -96,7 +96,7 @@ describe "Pipelines", feature: true, js: true do ...@@ -96,7 +96,7 @@ describe "Pipelines", feature: true, js: true do
end end
end end
context 'manual build' do context 'when pipeline has manual builds' do
it 'shows the skipped icon and a play action for the manual build' do it 'shows the skipped icon and a play action for the manual build' do
page.within('a[data-title="manual build - manual play action"]') do page.within('a[data-title="manual build - manual play action"]') do
expect(page).to have_selector('.ci-status-icon-skipped') expect(page).to have_selector('.ci-status-icon-skipped')
...@@ -115,7 +115,7 @@ describe "Pipelines", feature: true, js: true do ...@@ -115,7 +115,7 @@ describe "Pipelines", feature: true, js: true do
end end
end end
context 'external build' do context 'when pipeline has external build' do
it 'shows the success icon and the generic comit status build' do it 'shows the success icon and the generic comit status build' do
expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_selector('.ci-status-icon-success')
expect(page).to have_content('jenkins') expect(page).to have_content('jenkins')
......
...@@ -2,8 +2,8 @@ require 'spec_helper' ...@@ -2,8 +2,8 @@ require 'spec_helper'
feature 'Projects > Slack service > Setup events', feature: true do feature 'Projects > Slack service > Setup events', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:service) { SlackService.new } let(:service) { SlackNotificationService.new }
let(:project) { create(:project, slack_service: service) } let(:project) { create(:project, slack_notification_service: service) }
background do background do
service.fields service.fields
......
/* eslint-disable space-before-function-paren, no-new, padded-blocks */ /*= require lib/utils/text_utility */
/*= require abuse_reports */ /*= require abuse_reports */
/*= require jquery */
((global) => { ((global) => {
const FIXTURE = 'abuse_reports.html'; describe('Abuse Reports', () => {
const MAX_MESSAGE_LENGTH = 500; const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
const MAX_MESSAGE_LENGTH = 500;
let messages;
function assertMaxLength($message) { const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); const findMessage = searchText => messages.filter(
} (index, element) => element.innerText.indexOf(searchText) > -1,
).first();
describe('Abuse Reports', function() {
fixture.preload(FIXTURE); fixture.preload(FIXTURE);
beforeEach(function() { beforeEach(function () {
fixture.load(FIXTURE); fixture.load(FIXTURE);
new global.AbuseReports(); this.abuseReports = new global.AbuseReports();
messages = $('.abuse-reports .message');
}); });
it('should truncate long messages', function() {
const $longMessage = $('#long'); it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything()); expect($longMessage.data('original-message')).toEqual(jasmine.anything());
assertMaxLength($longMessage); assertMaxLength($longMessage);
}); });
it('should not truncate short messages', function() { it('should not truncate short messages', () => {
const $shortMessage = $('#short'); const $shortMessage = findMessage('SHORT MESSAGE');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
}); });
it('should allow clicking a truncated message to expand and collapse the full message', function() { it('should allow clicking a truncated message to expand and collapse the full message', () => {
const $longMessage = $('#long'); const $longMessage = findMessage('LONG MESSAGE');
$longMessage.click(); $longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click(); $longMessage.click();
assertMaxLength($longMessage); assertMaxLength($longMessage);
}); });
}); });
})(window.gl); })(window.gl);
.abuse-reports
.message#long
Cat ipsum dolor sit amet, hide head under blanket so no one can see.
Gate keepers of hell eat and than sleep on your face but hunt by meowing
loudly at 5am next to human slave food dispenser cats go for world
domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
cat is life chase after silly colored fish toys around the house climb a
tree, wait for a fireman jump to fireman then scratch his face fall asleep
on the washing machine lies down always hungry so caticus cuteicus. Sit on
human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
blanket so no one can see throwup on your pillow.
.message#short
Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
beauty sleep 18 hours - checked, be fabulous for the rest of the day -
checked! for shake treat bag.
require 'spec_helper'
describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let!(:abuse_report) { create(:abuse_report) }
let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') }
let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) }
render_views
before(:all) do
clean_frontend_fixtures('abuse_reports/')
end
before(:each) do
sign_in(admin)
end
it 'abuse_reports/abuse_reports_list.html.raw' do |example|
get :index
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
...@@ -136,7 +136,8 @@ project: ...@@ -136,7 +136,8 @@ project:
- assembla_service - assembla_service
- asana_service - asana_service
- gemnasium_service - gemnasium_service
- slack_service - slack_notification_service
- mattermost_notification_service
- buildkite_service - buildkite_service
- bamboo_service - bamboo_service
- teamcity_service - teamcity_service
......
...@@ -7,7 +7,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -7,7 +7,7 @@ describe Gitlab::Middleware::Multipart do
let(:middleware) { described_class.new(app) } let(:middleware) { described_class.new(app) }
it 'opens top-level files' do it 'opens top-level files' do
Tempfile.open do |tempfile| Tempfile.open('top-level') do |tempfile|
env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|
...@@ -33,7 +33,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -33,7 +33,7 @@ describe Gitlab::Middleware::Multipart do
end end
it 'opens files one level deep' do it 'opens files one level deep' do
Tempfile.open do |tempfile| Tempfile.open('one-level') do |tempfile|
in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } }
env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
...@@ -48,7 +48,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -48,7 +48,7 @@ describe Gitlab::Middleware::Multipart do
end end
it 'opens files two levels deep' do it 'opens files two levels deep' do
Tempfile.open do |tempfile| Tempfile.open('two-levels') do |tempfile|
in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } }
env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
......
...@@ -128,4 +128,26 @@ describe Namespace, models: true do ...@@ -128,4 +128,26 @@ describe Namespace, models: true do
it { expect(group.full_path).to eq(group.path) } it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
end end
describe '#full_name' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_name).to eq(group.name) }
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end
describe '#parents' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct parents' do
expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
expect(deep_nested_group.parents).to eq([group, nested_group])
expect(nested_group.parents).to eq([group])
expect(group.parents).to eq([])
end
end
end end
require 'spec_helper' require 'spec_helper'
describe SlackService::BuildMessage do describe ChatMessage::BuildMessage do
subject { SlackService::BuildMessage.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
{ {
......
require 'spec_helper' require 'spec_helper'
describe SlackService::IssueMessage, models: true do describe ChatMessage::IssueMessage, models: true do
subject { SlackService::IssueMessage.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
{ {
......
require 'spec_helper' require 'spec_helper'
describe SlackService::MergeMessage, models: true do describe ChatMessage::MergeMessage, models: true do
subject { SlackService::MergeMessage.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
{ {
......
require 'spec_helper' require 'spec_helper'
describe SlackService::NoteMessage, models: true do describe ChatMessage::NoteMessage, models: true do
let(:color) { '#345' } let(:color) { '#345' }
before do before do
...@@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do
end end
it 'returns a message regarding notes on commits' do it 'returns a message regarding notes on commits' do
message = SlackService::NoteMessage.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <url|commented on " \
"commit 5f163b2b> in <somewhere.com|project_name>: " \ "commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*") "*Added a commit message*")
...@@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do
end end
it 'returns a message regarding notes on a merge request' do it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <url|commented on " \
"merge request !30> in <somewhere.com|project_name>: " \ "merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*") "*merge request title*")
...@@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do
end end
it 'returns a message regarding notes on an issue' do it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq( expect(message.pretext).to eq(
"test.user <url|commented on " \ "test.user <url|commented on " \
"issue #20> in <somewhere.com|project_name>: " \ "issue #20> in <somewhere.com|project_name>: " \
...@@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do ...@@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do
end end
it 'returns a message regarding notes on a project snippet' do it 'returns a message regarding notes on a project snippet' do
message = SlackService::NoteMessage.new(@args) message = described_class.new(@args)
expect(message.pretext).to eq("test.user <url|commented on " \ expect(message.pretext).to eq("test.user <url|commented on " \
"snippet #5> in <somewhere.com|project_name>: " \ "snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*") "*snippet title*")
......
require 'spec_helper' require 'spec_helper'
describe SlackService::PipelineMessage do describe ChatMessage::PipelineMessage do
subject { SlackService::PipelineMessage.new(args) } subject { described_class.new(args) }
let(:user) { { name: 'hacker' } } let(:user) { { name: 'hacker' } }
let(:args) do let(:args) do
......
require 'spec_helper' require 'spec_helper'
describe SlackService::PushMessage, models: true do describe ChatMessage::PushMessage, models: true do
subject { SlackService::PushMessage.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
{ {
......
require 'spec_helper' require 'spec_helper'
describe SlackService::WikiPageMessage, models: true do describe ChatMessage::WikiPageMessage, models: true do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
require 'spec_helper'
describe ChatNotificationService, models: true do
describe "Associations" do
before do
allow(subject).to receive(:activated?).and_return(true)
end
it { is_expected.to validate_presence_of :webhook }
end
end
require 'spec_helper'
describe MattermostNotificationService, models: true do
it_behaves_like "slack or mattermost"
end
require 'spec_helper'
describe SlackNotificationService, models: true do
it_behaves_like "slack or mattermost"
end
...@@ -22,7 +22,8 @@ describe Project, models: true do ...@@ -22,7 +22,8 @@ describe Project, models: true do
it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
it { is_expected.to have_many(:chat_services) } it { is_expected.to have_many(:chat_services) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) }
......
require 'spec_helper'
describe GroupPolicy, models: true do
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let(:developer) { create(:user) }
let(:master) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group) }
let(:master_permissions) do
[
:create_projects,
:admin_milestones,
:admin_label
]
end
let(:owner_permissions) do
[
:admin_group,
:admin_namespace,
:admin_group_member,
:change_visibility_level
]
end
before do
group.add_guest(guest)
group.add_reporter(reporter)
group.add_developer(developer)
group.add_master(master)
group.add_owner(owner)
end
subject { described_class.abilities(current_user, group).to_set }
context 'with no user' do
let(:current_user) { nil }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'guests' do
let(:current_user) { guest }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'reporter' do
let(:current_user) { reporter }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'developer' do
let(:current_user) { developer }
it do
is_expected.to include(:read_group)
is_expected.not_to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'master' do
let(:current_user) { master }
it do
is_expected.to include(:read_group)
is_expected.to include(*master_permissions)
is_expected.not_to include(*owner_permissions)
end
end
context 'owner' do
let(:current_user) { owner }
it do
is_expected.to include(:read_group)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
end
context 'admin' do
let(:current_user) { admin }
it do
is_expected.to include(:read_group)
is_expected.to include(*master_permissions)
is_expected.to include(*owner_permissions)
end
end
end
...@@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do ...@@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do
let(:mentionable) { issue } let(:mentionable) { issue }
include_examples 'updating mentions', Issues::UpdateService include_examples 'updating mentions', Issues::UpdateService
end end
include_examples 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
end
end end
end end
...@@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do ...@@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do
expect(issue_ids).to be_empty expect(issue_ids).to be_empty
end end
end end
include_examples 'issuable update service' do
let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end
end end
end end
shared_examples 'issuable update service' do
context 'changing state' do
before { expect(project).to receive(:execute_hooks).once }
context 'to reopened' do
it 'executes hooks only once' do
described_class.new(project, user, state_event: 'reopen').execute(closed_issuable)
end
end
context 'to closed' do
it 'executes hooks only once' do
described_class.new(project, user, state_event: 'close').execute(open_issuable)
end
end
end
end
require 'spec_helper' Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
describe SlackService, models: true do RSpec.shared_examples 'slack or mattermost' do
let(:slack) { SlackService.new } let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' } let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do describe "Associations" do
...@@ -24,7 +24,7 @@ describe SlackService, models: true do ...@@ -24,7 +24,7 @@ describe SlackService, models: true do
end end
end end
describe "Execute" do describe "#execute" do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:username) { 'slack_username' } let(:username) { 'slack_username' }
...@@ -35,7 +35,7 @@ describe SlackService, models: true do ...@@ -35,7 +35,7 @@ describe SlackService, models: true do
end end
before do before do
allow(slack).to receive_messages( allow(chat_service).to receive_messages(
project: project, project: project,
project_id: project.id, project_id: project.id,
service_hook: true, service_hook: true,
...@@ -77,54 +77,55 @@ describe SlackService, models: true do ...@@ -77,54 +77,55 @@ describe SlackService, models: true do
@wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end end
it "calls Slack API for push events" do it "calls Slack/Mattermost API for push events" do
slack.execute(push_sample_data) chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
it "calls Slack API for issue events" do it "calls Slack/Mattermost API for issue events" do
slack.execute(@issues_sample_data) chat_service.execute(@issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
it "calls Slack API for merge requests events" do it "calls Slack/Mattermost API for merge requests events" do
slack.execute(@merge_sample_data) chat_service.execute(@merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
it "calls Slack API for wiki page events" do it "calls Slack/Mattermost API for wiki page events" do
slack.execute(@wiki_page_sample_data) chat_service.execute(@wiki_page_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
it 'uses the username as an option for slack when configured' do it 'uses the username as an option for slack when configured' do
allow(slack).to receive(:username).and_return(username) allow(chat_service).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, username: username). with(webhook_url, username: username, channel: chat_service.default_channel).
and_return( and_return(
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(push_sample_data) chat_service.execute(push_sample_data)
end end
it 'uses the channel as an option when it is configured' do it 'uses the channel as an option when it is configured' do
allow(slack).to receive(:channel).and_return(channel) allow(chat_service).to receive(:channel).and_return(channel)
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: channel). with(webhook_url, channel: channel).
and_return( and_return(
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(push_sample_data) chat_service.execute(push_sample_data)
end end
context "event channels" do context "event channels" do
it "uses the right channel for push event" do it "uses the right channel for push event" do
slack.update_attributes(push_channel: "random") chat_service.update_attributes(push_channel: "random")
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random"). with(webhook_url, channel: "random").
...@@ -132,11 +133,11 @@ describe SlackService, models: true do ...@@ -132,11 +133,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(push_sample_data) chat_service.execute(push_sample_data)
end end
it "uses the right channel for merge request event" do it "uses the right channel for merge request event" do
slack.update_attributes(merge_request_channel: "random") chat_service.update_attributes(merge_request_channel: "random")
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random"). with(webhook_url, channel: "random").
...@@ -144,11 +145,11 @@ describe SlackService, models: true do ...@@ -144,11 +145,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(@merge_sample_data) chat_service.execute(@merge_sample_data)
end end
it "uses the right channel for issue event" do it "uses the right channel for issue event" do
slack.update_attributes(issue_channel: "random") chat_service.update_attributes(issue_channel: "random")
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random"). with(webhook_url, channel: "random").
...@@ -156,11 +157,11 @@ describe SlackService, models: true do ...@@ -156,11 +157,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(@issues_sample_data) chat_service.execute(@issues_sample_data)
end end
it "uses the right channel for wiki event" do it "uses the right channel for wiki event" do
slack.update_attributes(wiki_page_channel: "random") chat_service.update_attributes(wiki_page_channel: "random")
expect(Slack::Notifier).to receive(:new). expect(Slack::Notifier).to receive(:new).
with(webhook_url, channel: "random"). with(webhook_url, channel: "random").
...@@ -168,7 +169,7 @@ describe SlackService, models: true do ...@@ -168,7 +169,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(@wiki_page_sample_data) chat_service.execute(@wiki_page_sample_data)
end end
context "note event" do context "note event" do
...@@ -177,7 +178,7 @@ describe SlackService, models: true do ...@@ -177,7 +178,7 @@ describe SlackService, models: true do
end end
it "uses the right channel" do it "uses the right channel" do
slack.update_attributes(note_channel: "random") chat_service.update_attributes(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user) note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
...@@ -187,7 +188,7 @@ describe SlackService, models: true do ...@@ -187,7 +188,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object double(:slack_service).as_null_object
) )
slack.execute(note_data) chat_service.execute(note_data)
end end
end end
end end
...@@ -198,7 +199,7 @@ describe SlackService, models: true do ...@@ -198,7 +199,7 @@ describe SlackService, models: true do
let(:project) { create(:project, creator_id: user.id) } let(:project) { create(:project, creator_id: user.id) }
before do before do
allow(slack).to receive_messages( allow(chat_service).to receive_messages(
project: project, project: project,
project_id: project.id, project_id: project.id,
service_hook: true, service_hook: true,
...@@ -216,9 +217,9 @@ describe SlackService, models: true do ...@@ -216,9 +217,9 @@ describe SlackService, models: true do
note: 'a comment on a commit') note: 'a comment on a commit')
end end
it "calls Slack API for commit comment events" do it "calls Slack/Mattermost API for commit comment events" do
data = Gitlab::DataBuilder::Note.build(commit_note, user) data = Gitlab::DataBuilder::Note.build(commit_note, user)
slack.execute(data) chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
...@@ -232,7 +233,7 @@ describe SlackService, models: true do ...@@ -232,7 +233,7 @@ describe SlackService, models: true do
it "calls Slack API for merge request comment events" do it "calls Slack API for merge request comment events" do
data = Gitlab::DataBuilder::Note.build(merge_request_note, user) data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
slack.execute(data) chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
...@@ -245,7 +246,7 @@ describe SlackService, models: true do ...@@ -245,7 +246,7 @@ describe SlackService, models: true do
it "calls Slack API for issue comment events" do it "calls Slack API for issue comment events" do
data = Gitlab::DataBuilder::Note.build(issue_note, user) data = Gitlab::DataBuilder::Note.build(issue_note, user)
slack.execute(data) chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
...@@ -259,7 +260,7 @@ describe SlackService, models: true do ...@@ -259,7 +260,7 @@ describe SlackService, models: true do
it "calls Slack API for snippet comment events" do it "calls Slack API for snippet comment events" do
data = Gitlab::DataBuilder::Note.build(snippet_note, user) data = Gitlab::DataBuilder::Note.build(snippet_note, user)
slack.execute(data) chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
...@@ -277,21 +278,21 @@ describe SlackService, models: true do ...@@ -277,21 +278,21 @@ describe SlackService, models: true do
end end
before do before do
allow(slack).to receive_messages( allow(chat_service).to receive_messages(
project: project, project: project,
service_hook: true, service_hook: true,
webhook: webhook_url webhook: webhook_url
) )
end end
shared_examples 'call Slack API' do shared_examples 'call Slack/Mattermost API' do
before do before do
WebMock.stub_request(:post, webhook_url) WebMock.stub_request(:post, webhook_url)
end end
it 'calls Slack API for pipeline events' do it 'calls Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline) data = Gitlab::DataBuilder::Pipeline.build(pipeline)
slack.execute(data) chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once expect(WebMock).to have_requested(:post, webhook_url).once
end end
...@@ -300,16 +301,16 @@ describe SlackService, models: true do ...@@ -300,16 +301,16 @@ describe SlackService, models: true do
context 'with failed pipeline' do context 'with failed pipeline' do
let(:status) { 'failed' } let(:status) { 'failed' }
it_behaves_like 'call Slack API' it_behaves_like 'call Slack/Mattermost API'
end end
context 'with succeeded pipeline' do context 'with succeeded pipeline' do
let(:status) { 'success' } let(:status) { 'success' }
context 'with default to notify_only_broken_pipelines' do context 'with default to notify_only_broken_pipelines' do
it 'does not call Slack API for pipeline events' do it 'does not call Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline) data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = slack.execute(data) result = chat_service.execute(data)
expect(result).to be_falsy expect(result).to be_falsy
end end
...@@ -317,10 +318,10 @@ describe SlackService, models: true do ...@@ -317,10 +318,10 @@ describe SlackService, models: true do
context 'with setting notify_only_broken_pipelines to false' do context 'with setting notify_only_broken_pipelines to false' do
before do before do
slack.notify_only_broken_pipelines = false chat_service.notify_only_broken_pipelines = false
end end
it_behaves_like 'call Slack API' it_behaves_like 'call Slack/Mattermost API'
end end
end end
end end
......
/*
The MIT License (MIT)
Copyright (c) 2015 Khan Academy
This software also uses portions of the underscore.js project, which is
MIT licensed with the following copyright:
Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
Reporters & Editors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
Here is how to build a version of KaTeX that works with gitlab.
The problem is that the standard procedure for changing font location doesn't work for the empty string.
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* eslint no-console:0 */ /* eslint no-console:0 */
/** /**
......
/*
The MIT License (MIT)
Copyright (c) 2015 Khan Academy
This software also uses portions of the underscore.js project, which is
MIT licensed with the following copyright:
Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
Reporters & Editors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
Here is how to build a version of KaTeX that works with gitlab.
The problem is that the standard procedure for changing font location doesn't work for the empty string.
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
*/
@font-face { @font-face {
font-family: 'KaTeX_AMS'; font-family: 'KaTeX_AMS';
src: url('KaTeX_AMS-Regular.eot'); src: url('KaTeX_AMS-Regular.eot');
......
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