diff --git a/CHANGELOG b/CHANGELOG index 031536816a5e17beea7afa2ee57c01c04a328fc4..7d61fef39a4525a7b7a92e2a38031ebb125ce25c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 6.7.0 - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) - Create and Update MR calls now support the description parameter (Greg Messner) - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository + - Added Slack service integration (Federico Ravasio) v 6.6.5 - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) diff --git a/Gemfile b/Gemfile index e3acf4a153d72c9d993c6a9dd9e867e4e7d0f3e4..6a587d1d279e6ed33cfd6e026a27a21736ad8c99 100644 --- a/Gemfile +++ b/Gemfile @@ -132,6 +132,9 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2" # Gemnasium integration gem "gemnasium-gitlab-service", "~> 0.2" +# Slack integration +gem "slack-notifier", "~> 0.2.0" + # d3 gem "d3_rails", "~> 3.1.4" diff --git a/Gemfile.lock b/Gemfile.lock index 52d6ac314633454bca33d6f7b146ac43c6fdc40b..de1ef59712ec067761b05f9b7049bf4658a3af5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -468,6 +468,7 @@ GEM rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) six (0.2.0) + slack-notifier (0.2.0) slim (2.0.2) temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) @@ -652,6 +653,7 @@ DEPENDENCIES simplecov sinatra six + slack-notifier (~> 0.2.0) slim spinach-rails spork (~> 1.0rc) diff --git a/app/models/project.rb b/app/models/project.rb index 7d7edc457391a94c34ea61f8f77f8b9d88e0eb18..769ab217625cb104545ce97bd17aee59dd0262bc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base has_one :flowdock_service, dependent: :destroy has_one :assembla_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy + has_one :slack_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it @@ -304,7 +305,7 @@ class Project < ActiveRecord::Base end def available_services_names - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium) + %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push gemnasium slack) end def gitlab_ci? diff --git a/app/models/project_services/slack_message.rb b/app/models/project_services/slack_message.rb new file mode 100644 index 0000000000000000000000000000000000000000..b2b8d6fed7a161151ec250e769cb40be6a82aacc --- /dev/null +++ b/app/models/project_services/slack_message.rb @@ -0,0 +1,95 @@ +require 'slack-notifier' + +class SlackMessage + def initialize(params) + @after = params.fetch(:after) + @before = params.fetch(:before) + @commits = params.fetch(:commits, []) + @project_name = params.fetch(:project_name) + @project_url = params.fetch(:project_url) + @ref = params.fetch(:ref).gsub('refs/heads/', '') + @username = params.fetch(:user_name) + end + + def compose + format(message) + end + + private + + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :project_name + attr_reader :project_url + attr_reader :ref + attr_reader :username + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message << commit_messages + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{username} pushed new branch #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{username} removed branch #{ref} from #{project_link}" + end + + def push_message + "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.each_with_object('') do |commit, str| + str << compose_commit_message(commit) + end + end + + def compose_commit_message(commit) + id = commit.fetch(:id)[0..5] + message = commit.fetch(:message) + url = commit.fetch(:url) + + "\n - #{message} ([#{id}](#{url}))" + end + + def new_branch? + before =~ /000000/ + end + + def removed_branch? + after =~ /000000/ + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end +end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..27648acf6d0c406ddb4d5e65d39a7f86fe848935 --- /dev/null +++ b/app/models/project_services/slack_service.rb @@ -0,0 +1,67 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# token :string(255) +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean default(FALSE), not null +# project_url :string(255) +# subdomain :string(255) +# room :string(255) +# api_key :string(255) +# + +class SlackService < Service + attr_accessible :room + attr_accessible :subdomain + + validates :room, presence: true, if: :activated? + validates :subdomain, presence: true, if: :activated? + validates :token, presence: true, if: :activated? + + def title + 'Slack' + end + + def description + 'A team communication tool for the 21st century' + end + + def to_param + 'slack' + end + + def fields + [ + { type: 'text', name: 'subdomain', placeholder: '' }, + { type: 'text', name: 'token', placeholder: '' }, + { type: 'text', name: 'room', placeholder: '' }, + ] + end + + def execute(push_data) + message = SlackMessage.new(push_data.merge( + project_url: project_url, + project_name: project_name + )) + + notifier = Slack::Notifier.new(subdomain, token) + notifier.channel = room + notifier.ping(message.compose) + end + + private + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end +end diff --git a/features/project/service.feature b/features/project/service.feature index 46b983e8f9a36d0312d47357b892dafd3dec915b..a5af065c9e7da2438d0f244208ca7418d516dde9 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -37,6 +37,12 @@ Feature: Project Services And I fill Assembla settings Then I should see Assembla service settings saved + Scenario: Activate Slack service + When I visit project "Shop" services page + And I click Slack service link + And I fill Slack settings + Then I should see Slack service settings saved + Scenario: Activate email on push service When I visit project "Shop" services page And I click email on push service link diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 54b3f18e0845a2f9503cb0f788c86fca9e1d60a2..0594a08a5e735c87009dcac9b03d8bd7a8516cf8 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps step 'I should see email on push service settings saved' do find_field('Recipients').value.should == 'qa@company.name' end + + step 'I click Slack service link' do + click_link 'Slack' + end + + step 'I fill Slack settings' do + check 'Active' + fill_in 'Subdomain', with: 'gitlab' + fill_in 'Room', with: '#gitlab' + fill_in 'Token', with: 'verySecret' + click_button 'Save' + end + + step 'I should see Slack service settings saved' do + find_field('Subdomain').value.should == 'gitlab' + find_field('Room').value.should == '#gitlab' + find_field('Token').value.should == 'verySecret' + end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 64ff7330d778835d73d0854c2ddb76bcddd437bd..d4d5e48ce3f47294134ed97faa509eb8dc0e92cd 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -66,6 +66,7 @@ namespace :gitlab do puts "no".green else puts "yes".red + puts "Please fix this by removing the SQLite entry from the database.yml".blue for_more_information( "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", see_database_guide diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6bae5951b7b2113ddcdd6ef5591ed50fe22a8ada..839350bafbfa2208360c8285f32c7f475359dc5d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -47,6 +47,7 @@ describe Project do it { should have_many(:hooks).dependent(:destroy) } it { should have_many(:protected_branches).dependent(:destroy) } it { should have_one(:forked_project_link).dependent(:destroy) } + it { should have_one(:slack_service).dependent(:destroy) } end describe "Mass assignment" do diff --git a/spec/models/slack_message_spec.rb b/spec/models/slack_message_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b39cd4edf820279d3c2fac76e775cb33ea044b62 --- /dev/null +++ b/spec/models/slack_message_spec.rb @@ -0,0 +1,56 @@ +require_relative '../../app/models/project_services/slack_message' + +describe SlackMessage do + subject { SlackMessage.new(args) } + + let(:args) { + { + after: 'after', + before: 'before', + project_name: 'project_name', + ref: 'refs/heads/master', + user_name: 'user_name', + project_url: 'url' + } + } + + context 'push' do + before do + args[:commits] = [ + { message: 'message1', url: 'url1', id: 'abcdefghi' }, + { message: 'message2', url: 'url2', id: '123456789' }, + ] + end + + it 'returns a message regarding pushes' do + subject.compose.should == + 'user_name pushed to branch of ' << + ' ()' << + "\n - message1 ()" << + "\n - message2 ()" + end + end + + context 'new branch' do + before do + args[:before] = '000000' + end + + it 'returns a message regarding a new branch' do + subject.compose.should == + 'user_name pushed new branch to ' << + '' + end + end + + context 'removed branch' do + before do + args[:after] = '000000' + end + + it 'returns a message regarding a removed branch' do + subject.compose.should == + 'user_name removed branch master from ' + end + end +end diff --git a/spec/models/slack_service_spec.rb b/spec/models/slack_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..387455cb25e01825012187c577fd372328515b7f --- /dev/null +++ b/spec/models/slack_service_spec.rb @@ -0,0 +1,69 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# token :string(255) +# project_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean default(FALSE), not null +# project_url :string(255) +# subdomain :string(255) +# room :string(255) +# api_key :string(255) +# + +require 'spec_helper' + +describe SlackService do + describe "Associations" do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { should validate_presence_of :room } + it { should validate_presence_of :subdomain } + it { should validate_presence_of :token } + end + end + + describe "Execute" do + let(:slack) { SlackService.new } + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:sample_data) { GitPushService.new.sample_data(project, user) } + let(:subdomain) { 'gitlab' } + let(:token) { 'verySecret' } + let(:api_url) { + "https://#{subdomain}.slack.com/services/hooks/incoming-webhook?token=#{token}" + } + + before do + slack.stub( + project: project, + project_id: project.id, + room: '#gitlab', + service_hook: true, + subdomain: subdomain, + token: token + ) + + WebMock.stub_request(:post, api_url) + end + + it "should call Slack API" do + slack.execute(sample_data) + + WebMock.should have_requested(:post, api_url).once + end + end +end