Commit d7b5656dab2a70d98491c453a654c243fd31a756
Exists in
spb-stable
and in
3 other branches
Merge branch 'master' of github.com:gitlabhq/gitlabhq
Showing
12 changed files
with
321 additions
and
1 deletions
Show diff stats
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 < 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 < 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? | ... | ... |
| ... | ... | @@ -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 | ... | ... |
| ... | ... | @@ -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 < 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 | ... | ... |
lib/tasks/gitlab/check.rake
| ... | ... | @@ -66,6 +66,7 @@ namespace :gitlab do |
| 66 | 66 | puts "no".green |
| 67 | 67 | else |
| 68 | 68 | puts "yes".red |
| 69 | + puts "Please fix this by removing the SQLite entry from the database.yml".blue | |
| 69 | 70 | for_more_information( |
| 70 | 71 | "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", |
| 71 | 72 | see_database_guide | ... | ... |
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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |