Commit e3fc01b3eaa13a51aef380dc21f1e7d259706227

Authored by Federico Ravasio
1 parent acaf2978

Added Slack service integration.

CHANGELOG
... ... @@ -24,6 +24,7 @@ v 6.7.0
24 24 - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5)
25 25 - Create and Update MR calls now support the description parameter (Greg Messner)
26 26 - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository
  27 + - Added Slack service integration (Federico Ravasio)
27 28  
28 29 v 6.6.5
29 30 - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard)
... ...
Gemfile
... ... @@ -132,6 +132,9 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2"
132 132 # Gemnasium integration
133 133 gem "gemnasium-gitlab-service", "~> 0.2"
134 134  
  135 +# Slack integration
  136 +gem "slack-notifier", "~> 0.2.0"
  137 +
135 138 # d3
136 139 gem "d3_rails", "~> 3.1.4"
137 140  
... ...
Gemfile.lock
... ... @@ -468,6 +468,7 @@ GEM
468 468 rack-protection (~> 1.4)
469 469 tilt (~> 1.3, >= 1.3.4)
470 470 six (0.2.0)
  471 + slack-notifier (0.2.0)
471 472 slim (2.0.2)
472 473 temple (~> 0.6.6)
473 474 tilt (>= 1.3.3, < 2.1)
... ... @@ -652,6 +653,7 @@ DEPENDENCIES
652 653 simplecov
653 654 sinatra
654 655 six
  656 + slack-notifier (~> 0.2.0)
655 657 slim
656 658 spinach-rails
657 659 spork (~> 1.0rc)
... ...
app/models/project.rb
... ... @@ -56,6 +56,7 @@ class Project &lt; ActiveRecord::Base
56 56 has_one :flowdock_service, dependent: :destroy
57 57 has_one :assembla_service, dependent: :destroy
58 58 has_one :gemnasium_service, dependent: :destroy
  59 + has_one :slack_service, dependent: :destroy
59 60 has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
60 61 has_one :forked_from_project, through: :forked_project_link
61 62 # Merge Requests for target project should be removed with it
... ... @@ -304,7 +305,7 @@ class Project &lt; ActiveRecord::Base
304 305 end
305 306  
306 307 def available_services_names
307   - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium)
  308 + %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack)
308 309 end
309 310  
310 311 def gitlab_ci?
... ...
app/models/project_services/slack_message.rb 0 → 100644
... ... @@ -0,0 +1,95 @@
  1 +require 'slack-notifier'
  2 +
  3 +class SlackMessage
  4 + def initialize(params)
  5 + @after = params.fetch(:after)
  6 + @before = params.fetch(:before)
  7 + @commits = params.fetch(:commits, [])
  8 + @project_name = params.fetch(:project_name)
  9 + @project_url = params.fetch(:project_url)
  10 + @ref = params.fetch(:ref).gsub('refs/heads/', '')
  11 + @username = params.fetch(:user_name)
  12 + end
  13 +
  14 + def compose
  15 + format(message)
  16 + end
  17 +
  18 + private
  19 +
  20 + attr_reader :after
  21 + attr_reader :before
  22 + attr_reader :commits
  23 + attr_reader :project_name
  24 + attr_reader :project_url
  25 + attr_reader :ref
  26 + attr_reader :username
  27 +
  28 + def message
  29 + if new_branch?
  30 + new_branch_message
  31 + elsif removed_branch?
  32 + removed_branch_message
  33 + else
  34 + push_message << commit_messages
  35 + end
  36 + end
  37 +
  38 + def format(string)
  39 + Slack::Notifier::LinkFormatter.format(string)
  40 + end
  41 +
  42 + def new_branch_message
  43 + "#{username} pushed new branch #{branch_link} to #{project_link}"
  44 + end
  45 +
  46 + def removed_branch_message
  47 + "#{username} removed branch #{ref} from #{project_link}"
  48 + end
  49 +
  50 + def push_message
  51 + "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})"
  52 + end
  53 +
  54 + def commit_messages
  55 + commits.each_with_object('') do |commit, str|
  56 + str << compose_commit_message(commit)
  57 + end
  58 + end
  59 +
  60 + def compose_commit_message(commit)
  61 + id = commit.fetch(:id)[0..5]
  62 + message = commit.fetch(:message)
  63 + url = commit.fetch(:url)
  64 +
  65 + "\n - #{message} ([#{id}](#{url}))"
  66 + end
  67 +
  68 + def new_branch?
  69 + before =~ /000000/
  70 + end
  71 +
  72 + def removed_branch?
  73 + after =~ /000000/
  74 + end
  75 +
  76 + def branch_url
  77 + "#{project_url}/commits/#{ref}"
  78 + end
  79 +
  80 + def compare_url
  81 + "#{project_url}/compare/#{before}...#{after}"
  82 + end
  83 +
  84 + def branch_link
  85 + "[#{ref}](#{branch_url})"
  86 + end
  87 +
  88 + def project_link
  89 + "[#{project_name}](#{project_url})"
  90 + end
  91 +
  92 + def compare_link
  93 + "[Compare changes](#{compare_url})"
  94 + end
  95 +end
... ...
app/models/project_services/slack_service.rb 0 → 100644
... ... @@ -0,0 +1,67 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: services
  4 +#
  5 +# id :integer not null, primary key
  6 +# type :string(255)
  7 +# title :string(255)
  8 +# token :string(255)
  9 +# project_id :integer not null
  10 +# created_at :datetime not null
  11 +# updated_at :datetime not null
  12 +# active :boolean default(FALSE), not null
  13 +# project_url :string(255)
  14 +# subdomain :string(255)
  15 +# room :string(255)
  16 +# api_key :string(255)
  17 +#
  18 +
  19 +class SlackService < Service
  20 + attr_accessible :room
  21 + attr_accessible :subdomain
  22 +
  23 + validates :room, presence: true, if: :activated?
  24 + validates :subdomain, presence: true, if: :activated?
  25 + validates :token, presence: true, if: :activated?
  26 +
  27 + def title
  28 + 'Slack'
  29 + end
  30 +
  31 + def description
  32 + 'A team communication tool for the 21st century'
  33 + end
  34 +
  35 + def to_param
  36 + 'slack'
  37 + end
  38 +
  39 + def fields
  40 + [
  41 + { type: 'text', name: 'subdomain', placeholder: '' },
  42 + { type: 'text', name: 'token', placeholder: '' },
  43 + { type: 'text', name: 'room', placeholder: '' },
  44 + ]
  45 + end
  46 +
  47 + def execute(push_data)
  48 + message = SlackMessage.new(push_data.merge(
  49 + project_url: project_url,
  50 + project_name: project_name
  51 + ))
  52 +
  53 + notifier = Slack::Notifier.new(subdomain, token)
  54 + notifier.channel = room
  55 + notifier.ping(message.compose)
  56 + end
  57 +
  58 + private
  59 +
  60 + def project_name
  61 + project.name_with_namespace.gsub(/\s/, '')
  62 + end
  63 +
  64 + def project_url
  65 + project.web_url
  66 + end
  67 +end
... ...
features/project/service.feature
... ... @@ -37,6 +37,12 @@ Feature: Project Services
37 37 And I fill Assembla settings
38 38 Then I should see Assembla service settings saved
39 39  
  40 + Scenario: Activate Slack service
  41 + When I visit project "Shop" services page
  42 + And I click Slack service link
  43 + And I fill Slack settings
  44 + Then I should see Slack service settings saved
  45 +
40 46 Scenario: Activate email on push service
41 47 When I visit project "Shop" services page
42 48 And I click email on push service link
... ...
features/steps/project/services.rb
... ... @@ -100,4 +100,22 @@ class ProjectServices &lt; Spinach::FeatureSteps
100 100 step 'I should see email on push service settings saved' do
101 101 find_field('Recipients').value.should == 'qa@company.name'
102 102 end
  103 +
  104 + step 'I click Slack service link' do
  105 + click_link 'Slack'
  106 + end
  107 +
  108 + step 'I fill Slack settings' do
  109 + check 'Active'
  110 + fill_in 'Subdomain', with: 'gitlab'
  111 + fill_in 'Room', with: '#gitlab'
  112 + fill_in 'Token', with: 'verySecret'
  113 + click_button 'Save'
  114 + end
  115 +
  116 + step 'I should see Slack service settings saved' do
  117 + find_field('Subdomain').value.should == 'gitlab'
  118 + find_field('Room').value.should == '#gitlab'
  119 + find_field('Token').value.should == 'verySecret'
  120 + end
103 121 end
... ...
spec/models/project_spec.rb
... ... @@ -47,6 +47,7 @@ describe Project do
47 47 it { should have_many(:hooks).dependent(:destroy) }
48 48 it { should have_many(:protected_branches).dependent(:destroy) }
49 49 it { should have_one(:forked_project_link).dependent(:destroy) }
  50 + it { should have_one(:slack_service).dependent(:destroy) }
50 51 end
51 52  
52 53 describe "Mass assignment" do
... ...
spec/models/slack_message_spec.rb 0 → 100644
... ... @@ -0,0 +1,56 @@
  1 +require_relative '../../app/models/project_services/slack_message'
  2 +
  3 +describe SlackMessage do
  4 + subject { SlackMessage.new(args) }
  5 +
  6 + let(:args) {
  7 + {
  8 + after: 'after',
  9 + before: 'before',
  10 + project_name: 'project_name',
  11 + ref: 'refs/heads/master',
  12 + user_name: 'user_name',
  13 + project_url: 'url'
  14 + }
  15 + }
  16 +
  17 + context 'push' do
  18 + before do
  19 + args[:commits] = [
  20 + { message: 'message1', url: 'url1', id: 'abcdefghi' },
  21 + { message: 'message2', url: 'url2', id: '123456789' },
  22 + ]
  23 + end
  24 +
  25 + it 'returns a message regarding pushes' do
  26 + subject.compose.should ==
  27 + 'user_name pushed to branch <url/commits/master|master> of ' <<
  28 + '<url|project_name> (<url/compare/before...after|Compare changes>)' <<
  29 + "\n - message1 (<url1|abcdef>)" <<
  30 + "\n - message2 (<url2|123456>)"
  31 + end
  32 + end
  33 +
  34 + context 'new branch' do
  35 + before do
  36 + args[:before] = '000000'
  37 + end
  38 +
  39 + it 'returns a message regarding a new branch' do
  40 + subject.compose.should ==
  41 + 'user_name pushed new branch <url/commits/master|master> to ' <<
  42 + '<url|project_name>'
  43 + end
  44 + end
  45 +
  46 + context 'removed branch' do
  47 + before do
  48 + args[:after] = '000000'
  49 + end
  50 +
  51 + it 'returns a message regarding a removed branch' do
  52 + subject.compose.should ==
  53 + 'user_name removed branch master from <url|project_name>'
  54 + end
  55 + end
  56 +end
... ...
spec/models/slack_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,69 @@
  1 +# == Schema Information
  2 +#
  3 +# Table name: services
  4 +#
  5 +# id :integer not null, primary key
  6 +# type :string(255)
  7 +# title :string(255)
  8 +# token :string(255)
  9 +# project_id :integer not null
  10 +# created_at :datetime not null
  11 +# updated_at :datetime not null
  12 +# active :boolean default(FALSE), not null
  13 +# project_url :string(255)
  14 +# subdomain :string(255)
  15 +# room :string(255)
  16 +# api_key :string(255)
  17 +#
  18 +
  19 +require 'spec_helper'
  20 +
  21 +describe SlackService do
  22 + describe "Associations" do
  23 + it { should belong_to :project }
  24 + it { should have_one :service_hook }
  25 + end
  26 +
  27 + describe "Validations" do
  28 + context "active" do
  29 + before do
  30 + subject.active = true
  31 + end
  32 +
  33 + it { should validate_presence_of :room }
  34 + it { should validate_presence_of :subdomain }
  35 + it { should validate_presence_of :token }
  36 + end
  37 + end
  38 +
  39 + describe "Execute" do
  40 + let(:slack) { SlackService.new }
  41 + let(:user) { create(:user) }
  42 + let(:project) { create(:project) }
  43 + let(:sample_data) { GitPushService.new.sample_data(project, user) }
  44 + let(:subdomain) { 'gitlab' }
  45 + let(:token) { 'verySecret' }
  46 + let(:api_url) {
  47 + "https://#{subdomain}.slack.com/services/hooks/incoming-webhook?token=#{token}"
  48 + }
  49 +
  50 + before do
  51 + slack.stub(
  52 + project: project,
  53 + project_id: project.id,
  54 + room: '#gitlab',
  55 + service_hook: true,
  56 + subdomain: subdomain,
  57 + token: token
  58 + )
  59 +
  60 + WebMock.stub_request(:post, api_url)
  61 + end
  62 +
  63 + it "should call Slack API" do
  64 + slack.execute(sample_data)
  65 +
  66 + WebMock.should have_requested(:post, api_url).once
  67 + end
  68 + end
  69 +end
... ...