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,6 +24,7 @@ v 6.7.0 | ||
| 24 | - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) | 24 | - Faster authorized_keys rebuilding in `rake gitlab:shell:setup` (requires gitlab-shell 1.8.5) |
| 25 | - Create and Update MR calls now support the description parameter (Greg Messner) | 25 | - Create and Update MR calls now support the description parameter (Greg Messner) |
| 26 | - Markdown relative links in the wiki link to wiki pages, markdown relative links in repositories link to files in the repository | 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 | v 6.6.5 | 29 | v 6.6.5 |
| 29 | - Added option to remove issue assignee on project issue page and issue edit page (Jason Blanchard) | 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,6 +132,9 @@ gem "gitlab-flowdock-git-hook", "~> 0.4.2" | ||
| 132 | # Gemnasium integration | 132 | # Gemnasium integration |
| 133 | gem "gemnasium-gitlab-service", "~> 0.2" | 133 | gem "gemnasium-gitlab-service", "~> 0.2" |
| 134 | 134 | ||
| 135 | +# Slack integration | ||
| 136 | +gem "slack-notifier", "~> 0.2.0" | ||
| 137 | + | ||
| 135 | # d3 | 138 | # d3 |
| 136 | gem "d3_rails", "~> 3.1.4" | 139 | gem "d3_rails", "~> 3.1.4" |
| 137 | 140 |
Gemfile.lock
| @@ -468,6 +468,7 @@ GEM | @@ -468,6 +468,7 @@ GEM | ||
| 468 | rack-protection (~> 1.4) | 468 | rack-protection (~> 1.4) |
| 469 | tilt (~> 1.3, >= 1.3.4) | 469 | tilt (~> 1.3, >= 1.3.4) |
| 470 | six (0.2.0) | 470 | six (0.2.0) |
| 471 | + slack-notifier (0.2.0) | ||
| 471 | slim (2.0.2) | 472 | slim (2.0.2) |
| 472 | temple (~> 0.6.6) | 473 | temple (~> 0.6.6) |
| 473 | tilt (>= 1.3.3, < 2.1) | 474 | tilt (>= 1.3.3, < 2.1) |
| @@ -652,6 +653,7 @@ DEPENDENCIES | @@ -652,6 +653,7 @@ DEPENDENCIES | ||
| 652 | simplecov | 653 | simplecov |
| 653 | sinatra | 654 | sinatra |
| 654 | six | 655 | six |
| 656 | + slack-notifier (~> 0.2.0) | ||
| 655 | slim | 657 | slim |
| 656 | spinach-rails | 658 | spinach-rails |
| 657 | spork (~> 1.0rc) | 659 | spork (~> 1.0rc) |
app/models/project.rb
| @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base | @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base | ||
| 56 | has_one :flowdock_service, dependent: :destroy | 56 | has_one :flowdock_service, dependent: :destroy |
| 57 | has_one :assembla_service, dependent: :destroy | 57 | has_one :assembla_service, dependent: :destroy |
| 58 | has_one :gemnasium_service, dependent: :destroy | 58 | has_one :gemnasium_service, dependent: :destroy |
| 59 | + has_one :slack_service, dependent: :destroy | ||
| 59 | has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" | 60 | has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" |
| 60 | has_one :forked_from_project, through: :forked_project_link | 61 | has_one :forked_from_project, through: :forked_project_link |
| 61 | # Merge Requests for target project should be removed with it | 62 | # Merge Requests for target project should be removed with it |
| @@ -304,7 +305,7 @@ class Project < ActiveRecord::Base | @@ -304,7 +305,7 @@ class Project < ActiveRecord::Base | ||
| 304 | end | 305 | end |
| 305 | 306 | ||
| 306 | def available_services_names | 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 | end | 309 | end |
| 309 | 310 | ||
| 310 | def gitlab_ci? | 311 | def gitlab_ci? |
| @@ -0,0 +1,95 @@ | @@ -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 @@ | @@ -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,6 +37,12 @@ Feature: Project Services | ||
| 37 | And I fill Assembla settings | 37 | And I fill Assembla settings |
| 38 | Then I should see Assembla service settings saved | 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 | Scenario: Activate email on push service | 46 | Scenario: Activate email on push service |
| 41 | When I visit project "Shop" services page | 47 | When I visit project "Shop" services page |
| 42 | And I click email on push service link | 48 | And I click email on push service link |
features/steps/project/services.rb
| @@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps | @@ -100,4 +100,22 @@ class ProjectServices < Spinach::FeatureSteps | ||
| 100 | step 'I should see email on push service settings saved' do | 100 | step 'I should see email on push service settings saved' do |
| 101 | find_field('Recipients').value.should == 'qa@company.name' | 101 | find_field('Recipients').value.should == 'qa@company.name' |
| 102 | end | 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 | end | 121 | end |
lib/tasks/gitlab/check.rake
| @@ -66,6 +66,7 @@ namespace :gitlab do | @@ -66,6 +66,7 @@ namespace :gitlab do | ||
| 66 | puts "no".green | 66 | puts "no".green |
| 67 | else | 67 | else |
| 68 | puts "yes".red | 68 | puts "yes".red |
| 69 | + puts "Please fix this by removing the SQLite entry from the database.yml".blue | ||
| 69 | for_more_information( | 70 | for_more_information( |
| 70 | "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", | 71 | "https://github.com/gitlabhq/gitlabhq/wiki/Migrate-from-SQLite-to-MySQL", |
| 71 | see_database_guide | 72 | see_database_guide |
spec/models/project_spec.rb
| @@ -47,6 +47,7 @@ describe Project do | @@ -47,6 +47,7 @@ describe Project do | ||
| 47 | it { should have_many(:hooks).dependent(:destroy) } | 47 | it { should have_many(:hooks).dependent(:destroy) } |
| 48 | it { should have_many(:protected_branches).dependent(:destroy) } | 48 | it { should have_many(:protected_branches).dependent(:destroy) } |
| 49 | it { should have_one(:forked_project_link).dependent(:destroy) } | 49 | it { should have_one(:forked_project_link).dependent(:destroy) } |
| 50 | + it { should have_one(:slack_service).dependent(:destroy) } | ||
| 50 | end | 51 | end |
| 51 | 52 | ||
| 52 | describe "Mass assignment" do | 53 | describe "Mass assignment" do |
| @@ -0,0 +1,56 @@ | @@ -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 @@ | @@ -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 |