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)
  ...
This commit is contained in:
Filipa Lacerda 2016-12-16 17:53:20 +00:00
commit 068a6599d8
94 changed files with 822 additions and 274 deletions

View File

@ -1 +1 @@
4.0.3
4.1.0

View File

@ -67,7 +67,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
gem 'grape', '~> 0.15.0'
gem 'grape', '~> 0.18.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'

View File

@ -284,15 +284,15 @@ GEM
json
multi_json
request_store (>= 1.0)
grape (0.15.0)
grape (0.18.0)
activesupport
builder
hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
mustermann-grape (~> 0.4.0)
rack (>= 1.3.0)
rack-accept
rack-mount
virtus (>= 1.0.0)
grape-entity (0.6.0)
activesupport
@ -400,6 +400,10 @@ GEM
multi_json (1.12.1)
multi_xml (0.5.5)
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)
net-ldap (0.12.1)
net-ssh (3.0.1)
@ -505,14 +509,12 @@ GEM
pry-rails (0.3.4)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.4)
rack (1.6.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
rack
rack-cors (0.4.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
rack-oauth2 (1.2.3)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
@ -743,6 +745,7 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
tool (0.2.3)
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
@ -861,7 +864,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
grape (~> 0.15.0)
grape (~> 0.18.0)
grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
hamlit (~> 2.6.1)

View File

@ -26,6 +26,10 @@ body {
.container-limited {
max-width: $fixed-layout-width;
&.limit-container-width {
max-width: $limited-layout-width;
}
}

View File

@ -154,6 +154,8 @@ $row-hover-border: #b2d7ff;
$progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$gl-avatar-size: 40px;
$error-exclamation-point: #e62958;
$border-radius-default: 2px;
$settings-icon-size: 18px;

View File

@ -1,3 +1,50 @@
// 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 {
section {
.issuable-discussion {
@ -9,7 +56,6 @@
.description img:not(.emoji) {
border: 1px solid $white-normal;
padding: 5px;
margin: 5px;
max-height: calc(100vh - 100px);
}
}

View File

@ -383,10 +383,6 @@ ul.notes {
.note-action-button {
margin-left: 10px;
}
@media (min-width: $screen-sm-min) {
position: relative;
}
}
.discussion-actions {

View File

@ -616,14 +616,10 @@
li {
padding-top: 2px;
margin: 0 5px;
}
li:first-child {
padding-top: 6px;
}
li:last-child {
padding-bottom: 6px;
padding-left: 0;
padding-bottom: 0;
margin-bottom: 0;
line-height: 1.2;
}
}
}

View File

@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
@qr_code = build_qr_code
@account_string = account_string
setup_u2f_registration
end
@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
private
def build_qr_code
issuer = "#{issuer_host} | #{current_user.email}"
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
def account_string
"#{issuer_host}:#{current_user.email}"
end
def issuer_host
Gitlab.config.gitlab.host
end

View File

@ -12,11 +12,18 @@ module GroupsHelper
end
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
content_tag :span do
full_title
full_title.html_safe
end
end

View File

@ -52,7 +52,7 @@ module ProjectsHelper
def project_title(project)
namespace_link =
if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group))
group_title(project.group)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
@ -390,7 +390,7 @@ module ProjectsHelper
"success"
end
end
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')

View File

@ -83,7 +83,7 @@ class Group < Namespace
end
def human_name
name
full_name
end
def visibility_level_field

View File

@ -161,6 +161,19 @@ class Namespace < ActiveRecord::Base
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
def repository_storage_paths

View File

@ -95,7 +95,8 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy
has_one :gemnasium_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 :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy

View File

@ -1,6 +1,6 @@
require 'slack-notifier'
class SlackService
module ChatMessage
class BaseMessage
def initialize(params)
raise NotImplementedError

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :user_name
attr_reader :title

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :user_name
attr_reader :project_name

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :message
attr_reader :user_name

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class PipelineMessage < BaseMessage
attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before

View File

@ -1,4 +1,4 @@
class SlackService
module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title

View File

@ -1,6 +1,13 @@
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
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
@ -14,35 +21,8 @@ class SlackService < Service
end
end
def title
'Slack'
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
def can_test?
valid?
end
def supported_events
@ -67,21 +47,16 @@ class SlackService < Service
message = get_message(object_kind, data)
if message
opt = {}
return false unless message
event_channel = get_channel_field(object_kind) || channel
opt = {}
opt[:channel] = event_channel if event_channel
opt[:username] = username if username
opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
else
false
end
true
end
def event_channel_names
@ -96,6 +71,10 @@ class SlackService < Service
fields.reject { |field| field[:name].end_with?('channel') }
end
def default_channel
raise NotImplementedError
end
private
def get_message(object_kind, data)
@ -124,7 +103,7 @@ class SlackService < Service
def build_event_channels
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
@ -166,11 +145,3 @@ class SlackService < Service
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"

View File

@ -1,5 +1,5 @@
# 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
default_value_for :category, 'chat'

View File

@ -0,0 +1,41 @@
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

View File

@ -0,0 +1,40 @@
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

View File

@ -220,7 +220,8 @@ class Service < ActiveRecord::Base
pivotaltracker
pushover
redmine
slack
mattermost_notification
slack_notification
teamcity
]
end

View File

@ -184,7 +184,8 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.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)
# We do not touch as it will affect a update on updated_at field
@ -201,6 +202,10 @@ class IssuableBaseService < BaseService
issuable
end
def labels_changing?(old_label_ids, new_label_ids)
old_label_ids.sort != new_label_ids.sort
end
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'

View File

@ -20,7 +20,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
= group.full_name
- if group.description.present?
.description

View File

@ -1,6 +1,6 @@
- page_title @group.name, "Groups"
%h3.page-title
Group: #{@group.name}
Group: #{@group.full_name}
= link_to admin_group_edit_path(@group), class: "btn pull-right" do
%i.fa.fa-pencil-square-o

View File

@ -3,10 +3,9 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
- klass = "ci-status-icon ci-status-icon-#{status}"
- tooltip_title = "#{subject.name} - #{status.label}"
- 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)
.ci-status-text= subject.name
- else
@ -15,6 +14,6 @@
- if status.has_action?
= 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
= icon(status.action_icon, class: status.action_class)

View File

@ -30,7 +30,7 @@
To add the entry manually, provide the following details to the application on your phone.
%p.prepend-top-0.append-bottom-0
Account:
= current_user.email
= @account_string
%p.prepend-top-0.append-bottom-0
Key:
= current_user.otp_secret.scan(/.{4}/).join(' ')

View File

@ -1,13 +0,0 @@
- 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)

View File

@ -1,10 +0,0 @@
%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

View File

@ -1,3 +1,4 @@
- @content_class = "limit-container-width"
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes

View File

@ -1,3 +1,4 @@
- @content_class = "limit-container-width"
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
@ -41,7 +42,7 @@
= 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)
.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
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"

View File

@ -28,7 +28,7 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
= group.name
= group.full_name
- if group_member
as

View File

@ -0,0 +1,4 @@
---
title: Ensure issuable state changes only fire webhooks once
merge_request:
author:

View File

@ -0,0 +1,4 @@
---
title: Replace static fixture for abuse_reports_spec
merge_request: 7644
author: winniehell

View File

@ -0,0 +1,4 @@
---
title: Add GitLab host to 2FA QR code and manual info
merge_request: 6941
author:

View File

@ -0,0 +1,4 @@
---
title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
merge_request:
author: twonegatives

View File

@ -0,0 +1,4 @@
---
title: 'Gem update: Update grape to 0.18.0'
merge_request:
author: Robert Schilling

View File

@ -0,0 +1,4 @@
---
title: Create mattermost service
merge_request:
author:

View File

@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
def job_attributes(pipeline, opts)
{ 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
}.merge(opts)
end

View File

@ -1,7 +1,11 @@
# rubocop:disable all
class MoveSlackServiceToWebhook < ActiveRecord::Migration
DOWNTIME = true
DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
def change
SlackService.all.each do |slack_service|
SlackNotificationService.all.each do |slack_service|
if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
token = slack_service.properties['token']
subdomain = slack_service.properties['subdomain']

View File

@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
end
def down
Route.delete_all(source_type: 'Namespace')
execute("DELETE FROM routes WHERE source_type = 'Namespace'")
end
end

View File

@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
DOWNTIME_REASON = 'No new projects should be created during data copy'
def up
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
if Gitlab::Database.postgresql?
execute <<-EOF
INSERT INTO routes (source_id, source_type, path)
(SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
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
def down
Route.delete_all(source_type: 'Project')
execute("DELETE FROM routes WHERE source_type = 'Project'")
end
end

View File

@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
DOWNTIME = false
def up
select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row|
path = connection.quote(row['path'])
execute(%Q{
DELETE FROM #{quote_table_name(:routes)}
WHERE path = #{path}
AND id != (
SELECT id FROM (
SELECT max(id) AS id
FROM #{quote_table_name(:routes)}
WHERE path = #{path}
) max_ids
)
})
end
# We can skip this migration when running a PostgreSQL database because
# we use an optimized query in the "FillProjectsRoutesTable" migration
# to fill these values that avoid duplicate entries in the routes table.
return unless Gitlab::Database.mysql?
execute <<-EOF
DELETE duplicated_rows.*
FROM routes AS duplicated_rows
INNER JOIN (
SELECT path, MAX(id) as max_id
FROM routes
GROUP BY path
HAVING COUNT(*) > 1
) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
EOF
end
def down

View File

@ -13,7 +13,7 @@ class AddNameIndexToNamespace < ActiveRecord::Migration
end
def down
if index_exists?(:namespaces, :name)
if index_exists?(:namespaces, [:name, :parent_id])
remove_index :namespaces, [:name, :parent_id]
end
end

View File

@ -0,0 +1,14 @@
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

View File

@ -11,7 +11,7 @@
#
# 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
enable_extension "plpgsql"
@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.text "help_page_text_html"
t.text "shared_runners_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_bitmaps_enabled", default: true, 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_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
end
@ -527,6 +527,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.string "type"
t.string "fingerprint"
t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
@ -739,8 +740,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "lfs_enabled"
t.integer "parent_id"
end
@ -1221,8 +1222,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
t.string "organization"
t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated"
end

View File

@ -44,22 +44,30 @@ as appropriate.
## 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)
directories supporting chained execution of the hooks.
Hooks can be also placed in `hooks/<hook_name>.d` (global) or
`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:
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/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/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
script exiting with non-zero value.
The hooks of the same type are executed in order and execution stops on the
first script exiting with a non-zero value.
## Custom error messages

View File

@ -703,9 +703,9 @@ Get Redmine service settings for a project.
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
@ -737,6 +737,40 @@ Get Slack service settings for a project.
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
A continuous integration and build server

View File

@ -5,6 +5,7 @@
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
* [Cursors](#cursors)
---
@ -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.
---
## 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. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -0,0 +1,45 @@
# 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)

View File

@ -44,10 +44,11 @@ further configuration instructions and details. Contributions are welcome.
| JetBrains TeamCity CI | A continuous integration and build server |
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [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) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
| [Slack](slack.md) | A team communication tool for the 21st century |
## Services Templates

View File

@ -1,4 +1,4 @@
# Slack Service
# Slack Notifications Service
## On Slack
@ -15,7 +15,7 @@ Slack:
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:
- Push

View File

@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
```bash
cd /home/git/gitlab-shell
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

View File

@ -473,7 +473,7 @@ module API
desc: 'The description of the tracker'
}
],
'slack' => [
'slack-notification' => [
{
required: true,
name: :webhook,
@ -493,6 +493,14 @@ module API
desc: 'The channel name'
}
],
'mattermost-notification' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
}
],
'teamcity' => [
{
required: true,

View File

@ -107,7 +107,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build' do
visit ci_status_path(pipeline)
click_on 'Cancel'
find('a.btn[title="Cancel"]').click
expect(page).to have_content 'canceled'
end
end

View File

@ -38,8 +38,8 @@ describe "Pipelines", feature: true, js: true do
expect(page).to have_css('#js-tab-pipeline.active')
end
context 'pipeline graph' do
context 'running build' do
describe 'pipeline graph' do
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
page.within('a[data-title="deploy - running"]') do
expect(page).to have_selector('.ci-status-icon-running')
@ -58,7 +58,7 @@ describe "Pipelines", feature: true, js: true do
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
page.within('a[data-title="build - passed"]') do
expect(page).to have_selector('.ci-status-icon-success')
@ -77,7 +77,7 @@ describe "Pipelines", feature: true, js: true do
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
page.within('a[data-title="test - failed"]') do
expect(page).to have_selector('.ci-status-icon-failed')
@ -96,7 +96,7 @@ describe "Pipelines", feature: true, js: true do
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
page.within('a[data-title="manual build - manual play action"]') do
expect(page).to have_selector('.ci-status-icon-skipped')
@ -115,7 +115,7 @@ describe "Pipelines", feature: true, js: true do
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
expect(page).to have_selector('.ci-status-icon-success')
expect(page).to have_content('jenkins')

View File

@ -2,8 +2,8 @@ require 'spec_helper'
feature 'Projects > Slack service > Setup events', feature: true do
let(:user) { create(:user) }
let(:service) { SlackService.new }
let(:project) { create(:project, slack_service: service) }
let(:service) { SlackNotificationService.new }
let(:project) { create(:project, slack_notification_service: service) }
background do
service.fields

View File

@ -1,42 +1,44 @@
/* eslint-disable space-before-function-paren, no-new, padded-blocks */
/*= require lib/utils/text_utility */
/*= require abuse_reports */
/*= require jquery */
((global) => {
const FIXTURE = 'abuse_reports.html';
const MAX_MESSAGE_LENGTH = 500;
describe('Abuse Reports', () => {
const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
const MAX_MESSAGE_LENGTH = 500;
function assertMaxLength($message) {
expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
}
let messages;
const assertMaxLength = $message => 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);
beforeEach(function() {
beforeEach(function () {
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());
assertMaxLength($longMessage);
});
it('should not truncate short messages', function() {
const $shortMessage = $('#short');
it('should not truncate short messages', () => {
const $shortMessage = findMessage('SHORT MESSAGE');
expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', function() {
const $longMessage = $('#long');
it('should allow clicking a truncated message to expand and collapse the full message', () => {
const $longMessage = findMessage('LONG MESSAGE');
$longMessage.click();
expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
$longMessage.click();
assertMaxLength($longMessage);
});
});
})(window.gl);

View File

@ -1,16 +0,0 @@
.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.

View File

@ -0,0 +1,27 @@
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

View File

@ -136,7 +136,8 @@ project:
- assembla_service
- asana_service
- gemnasium_service
- slack_service
- slack_notification_service
- mattermost_notification_service
- buildkite_service
- bamboo_service
- teamcity_service

View File

@ -7,7 +7,7 @@ describe Gitlab::Middleware::Multipart do
let(:middleware) { described_class.new(app) }
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')
expect(app).to receive(:call) do |env|
@ -33,7 +33,7 @@ describe Gitlab::Middleware::Multipart do
end
it 'opens files one level deep' do
Tempfile.open do |tempfile|
Tempfile.open('one-level') do |tempfile|
in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } }
env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
@ -48,7 +48,7 @@ describe Gitlab::Middleware::Multipart do
end
it 'opens files two levels deep' do
Tempfile.open do |tempfile|
Tempfile.open('two-levels') do |tempfile|
in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } }
env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')

View File

@ -128,4 +128,26 @@ describe Namespace, models: true do
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
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

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe SlackService::BuildMessage do
subject { SlackService::BuildMessage.new(args) }
describe ChatMessage::BuildMessage do
subject { described_class.new(args) }
let(:args) do
{

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe SlackService::IssueMessage, models: true do
subject { SlackService::IssueMessage.new(args) }
describe ChatMessage::IssueMessage, models: true do
subject { described_class.new(args) }
let(:args) do
{

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe SlackService::MergeMessage, models: true do
subject { SlackService::MergeMessage.new(args) }
describe ChatMessage::MergeMessage, models: true do
subject { described_class.new(args) }
let(:args) do
{

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe SlackService::NoteMessage, models: true do
describe ChatMessage::NoteMessage, models: true do
let(:color) { '#345' }
before do
@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do
end
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 " \
"commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*")
@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do
end
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 " \
"merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do
end
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(
"test.user <url|commented on " \
"issue #20> in <somewhere.com|project_name>: " \
@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do
end
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 " \
"snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*")

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe SlackService::PipelineMessage do
subject { SlackService::PipelineMessage.new(args) }
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
let(:user) { { name: 'hacker' } }
let(:args) do

View File

@ -1,7 +1,7 @@
require 'spec_helper'
describe SlackService::PushMessage, models: true do
subject { SlackService::PushMessage.new(args) }
describe ChatMessage::PushMessage, models: true do
subject { described_class.new(args) }
let(:args) do
{

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe SlackService::WikiPageMessage, models: true do
describe ChatMessage::WikiPageMessage, models: true do
subject { described_class.new(args) }
let(:args) do

View File

@ -0,0 +1,11 @@
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

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe MattermostNotificationService, models: true do
it_behaves_like "slack or mattermost"
end

View File

@ -0,0 +1,5 @@
require 'spec_helper'
describe SlackNotificationService, models: true do
it_behaves_like "slack or mattermost"
end

View File

@ -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(:chat_services) }
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(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:boards).dependent(:destroy) }

View File

@ -0,0 +1,108 @@
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

View File

@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do
let(:mentionable) { issue }
include_examples 'updating mentions', Issues::UpdateService
end
include_examples 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
end
end
end

View File

@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do
expect(issue_ids).to be_empty
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

View File

@ -0,0 +1,17 @@
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

View File

@ -1,7 +1,7 @@
require 'spec_helper'
Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
describe SlackService, models: true do
let(:slack) { SlackService.new }
RSpec.shared_examples 'slack or mattermost' do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
describe "Associations" do
@ -24,7 +24,7 @@ describe SlackService, models: true do
end
end
describe "Execute" do
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:username) { 'slack_username' }
@ -35,7 +35,7 @@ describe SlackService, models: true do
end
before do
allow(slack).to receive_messages(
allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
@ -77,54 +77,55 @@ describe SlackService, models: true do
@wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
it "calls Slack API for push events" do
slack.execute(push_sample_data)
it "calls Slack/Mattermost API for push events" do
chat_service.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it "calls Slack API for issue events" do
slack.execute(@issues_sample_data)
it "calls Slack/Mattermost API for issue events" do
chat_service.execute(@issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it "calls Slack API for merge requests events" do
slack.execute(@merge_sample_data)
it "calls Slack/Mattermost API for merge requests events" do
chat_service.execute(@merge_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
it "calls Slack API for wiki page events" do
slack.execute(@wiki_page_sample_data)
it "calls Slack/Mattermost API for wiki page events" do
chat_service.execute(@wiki_page_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
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).
with(webhook_url, username: username).
with(webhook_url, username: username, channel: chat_service.default_channel).
and_return(
double(:slack_service).as_null_object
)
slack.execute(push_sample_data)
chat_service.execute(push_sample_data)
end
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).
with(webhook_url, channel: channel).
and_return(
double(:slack_service).as_null_object
)
slack.execute(push_sample_data)
chat_service.execute(push_sample_data)
end
context "event channels" 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).
with(webhook_url, channel: "random").
@ -132,11 +133,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
slack.execute(push_sample_data)
chat_service.execute(push_sample_data)
end
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).
with(webhook_url, channel: "random").
@ -144,11 +145,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
slack.execute(@merge_sample_data)
chat_service.execute(@merge_sample_data)
end
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).
with(webhook_url, channel: "random").
@ -156,11 +157,11 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
slack.execute(@issues_sample_data)
chat_service.execute(@issues_sample_data)
end
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).
with(webhook_url, channel: "random").
@ -168,7 +169,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
slack.execute(@wiki_page_sample_data)
chat_service.execute(@wiki_page_sample_data)
end
context "note event" do
@ -177,7 +178,7 @@ describe SlackService, models: true do
end
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)
@ -187,7 +188,7 @@ describe SlackService, models: true do
double(:slack_service).as_null_object
)
slack.execute(note_data)
chat_service.execute(note_data)
end
end
end
@ -198,7 +199,7 @@ describe SlackService, models: true do
let(:project) { create(:project, creator_id: user.id) }
before do
allow(slack).to receive_messages(
allow(chat_service).to receive_messages(
project: project,
project_id: project.id,
service_hook: true,
@ -216,9 +217,9 @@ describe SlackService, models: true do
note: 'a comment on a commit')
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)
slack.execute(data)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@ -232,7 +233,7 @@ describe SlackService, models: true do
it "calls Slack API for merge request comment events" do
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
end
@ -245,7 +246,7 @@ describe SlackService, models: true do
it "calls Slack API for issue comment events" do
data = Gitlab::DataBuilder::Note.build(issue_note, user)
slack.execute(data)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@ -259,7 +260,7 @@ describe SlackService, models: true do
it "calls Slack API for snippet comment events" do
data = Gitlab::DataBuilder::Note.build(snippet_note, user)
slack.execute(data)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@ -277,21 +278,21 @@ describe SlackService, models: true do
end
before do
allow(slack).to receive_messages(
allow(chat_service).to receive_messages(
project: project,
service_hook: true,
webhook: webhook_url
)
end
shared_examples 'call Slack API' do
shared_examples 'call Slack/Mattermost API' do
before do
WebMock.stub_request(:post, webhook_url)
end
it 'calls Slack API for pipeline events' do
it 'calls Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
slack.execute(data)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
@ -300,16 +301,16 @@ describe SlackService, models: true do
context 'with failed pipeline' do
let(:status) { 'failed' }
it_behaves_like 'call Slack API'
it_behaves_like 'call Slack/Mattermost API'
end
context 'with succeeded pipeline' do
let(:status) { 'success' }
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)
result = slack.execute(data)
result = chat_service.execute(data)
expect(result).to be_falsy
end
@ -317,10 +318,10 @@ describe SlackService, models: true do
context 'with setting notify_only_broken_pipelines to false' do
before do
slack.notify_only_broken_pipelines = false
chat_service.notify_only_broken_pipelines = false
end
it_behaves_like 'call Slack API'
it_behaves_like 'call Slack/Mattermost API'
end
end
end

View File

@ -1,3 +1,44 @@
/*
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){
/* eslint no-console:0 */
/**

View File

@ -1,3 +1,44 @@
/*
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-family: 'KaTeX_AMS';
src: url('KaTeX_AMS-Regular.eot');