summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAorimn <aorimn@gmail.com>2015-02-04 21:31:55 +0100
committerAorimn <aorimn@gmail.com>2015-03-01 20:51:03 +0100
commitf84b7eef3f969a65d0930c9d62b6968b2ae70f12 (patch)
treee365fd8d4fff95ebeb809796726fe70fafe75004
parent4e5bc1d5356e860be316656fd8c3393c468d24b6 (diff)
downloadgitlab-ce-f84b7eef3f969a65d0930c9d62b6968b2ae70f12.tar.gz
Add Irker service
Irker is a gateway which sends IRC messages on git updates. This new service provides an interface to this gateway, integrated in Gitlab, for each updates. As per the guidelines, this commit adds the new feature in the CHANGELOG, tests and documentation. See http://www.catb.org/esr/irker/
-rw-r--r--CHANGELOG1
-rw-r--r--app/controllers/projects/services_controller.rb3
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_services/irker_service.rb152
-rw-r--r--app/models/service.rb3
-rw-r--r--app/workers/irker_worker.rb169
-rw-r--r--doc/project_services/irker.md46
-rw-r--r--doc/project_services/project_services.md1
-rw-r--r--features/project/service.feature6
-rw-r--r--features/steps/project/services.rb17
-rw-r--r--spec/models/project_services/irker_service_spec.rb103
11 files changed, 500 insertions, 2 deletions
diff --git a/CHANGELOG b/CHANGELOG
index dae32953cd9..ee862a4ca3c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -14,6 +14,7 @@ v 7.9.0 (unreleased)
- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
- Fix mass-unassignment of issues (Robert Speicher)
- Allow user confirmation to be skipped for new users via API
+ - Add a service to send updates to an Irker gateway (Romain Coltel)
v 7.8.1
- Fix run of custom post receive hooks
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 5c29a6550f5..e7823020e60 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -50,7 +50,8 @@ class Projects::ServicesController < Projects::ApplicationController
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch
+ :description, :issues_url, :new_issue_url, :restrict_to_branch,
+ :colorize_messages, :channels
)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 7f2e0b4c17b..907f331d8f1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base
has_one :gitlab_ci_service, dependent: :destroy
has_one :campfire_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
+ has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
has_one :flowdock_service, dependent: :destroy
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
new file mode 100644
index 00000000000..a0203a5bb10
--- /dev/null
+++ b/app/models/project_services/irker_service.rb
@@ -0,0 +1,152 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+
+require 'uri'
+
+class IrkerService < Service
+ prop_accessor :colorize_messages, :recipients, :channels
+ validates :recipients, presence: true, if: :activated?
+ validate :check_recipients_count, if: :activated?
+
+ before_validation :get_channels
+ after_initialize :initialize_settings
+
+ # Writer for RSpec tests
+ attr_writer :settings
+
+ def initialize_settings
+ # See the documentation (doc/project_services/irker.md) for possible values
+ # here
+ @settings ||= {
+ server_ip: 'localhost',
+ server_port: 6659,
+ max_channels: 3,
+ default_irc_uri: nil
+ }
+ end
+
+ def title
+ 'Irker (IRC gateway)'
+ end
+
+ def description
+ 'Send IRC messages, on update, to a list of recipients through an Irker '\
+ 'gateway.'
+ end
+
+ def help
+ msg = 'Recipients have to be specified with a full URI: '\
+ 'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\
+ 'the channel to be a nickname instead, append ",isnick" to the channel '\
+ 'name; if the channel is protected by a secret password, append '\
+ '"?key=secretpassword" to the URI.'
+
+ unless @settings[:default_irc].nil?
+ msg += ' Note that a default IRC URI is provided by this service\'s '\
+ "administrator: #{default_irc}. You can thus just give a channel name."
+ end
+ msg
+ end
+
+ def to_param
+ 'irker'
+ end
+
+ def execute(push_data)
+ IrkerWorker.perform_async(project_id, channels,
+ colorize_messages, push_data, @settings)
+ end
+
+ def fields
+ [
+ { type: 'textarea', name: 'recipients',
+ placeholder: 'Recipients/channels separated by whitespaces' },
+ { type: 'checkbox', name: 'colorize_messages' },
+ ]
+ end
+
+ private
+
+ def check_recipients_count
+ return true if recipients.nil? || recipients.empty?
+
+ if recipients.split(/\s+/).count > max_chans
+ errors.add(:recipients, "are limited to #{max_chans}")
+ end
+ end
+
+ def max_chans
+ @settings[:max_channels]
+ end
+
+ def get_channels
+ return true unless :activated?
+ return true if recipients.nil? || recipients.empty?
+
+ map_recipients
+
+ errors.add(:recipients, 'are all invalid') if channels.empty?
+ true
+ end
+
+ def map_recipients
+ self.channels = recipients.split(/\s+/).map do |recipient|
+ format_channel default_irc_uri, recipient
+ end
+ channels.reject! &:nil?
+ end
+
+ def default_irc_uri
+ default_irc = @settings[:default_irc_uri]
+ if !(default_irc.nil? || default_irc[-1] == '/')
+ default_irc += '/'
+ end
+ default_irc
+ end
+
+ def format_channel(default_irc, recipient)
+ cnt = 0
+ url = nil
+
+ # Try to parse the chan as a full URI
+ begin
+ uri = URI.parse(recipient)
+ raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0
+ rescue URI::InvalidURIError
+ unless default_irc.nil?
+ cnt += 1
+ recipient = "#{default_irc}#{recipient}"
+ retry if cnt == 1
+ end
+ else
+ url = consider_uri uri
+ end
+ url
+ end
+
+ def consider_uri(uri)
+ # Authorize both irc://domain.com/#chan and irc://domain.com/chan
+ if uri.is_a?(URI) && uri.scheme[/^ircs?$/] && !uri.path.nil?
+ # Do not authorize irc://domain.com/
+ if uri.fragment.nil? && uri.path.length > 1
+ uri.to_s
+ else
+ # Authorize irc://domain.com/smthg#chan
+ # The irker daemon will deal with it by concatenating smthg and
+ # chan, thus sending messages on #smthgchan
+ uri.to_s
+ end
+ end
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index f87d875c10a..f4e97da3212 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -100,7 +100,8 @@ class Service < ActiveRecord::Base
def self.available_services_names
%w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana
- emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker)
+ emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira
+ redmine custom_issue_tracker irker)
end
def self.create_from_template(project_id, template)
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
new file mode 100644
index 00000000000..613bae351d8
--- /dev/null
+++ b/app/workers/irker_worker.rb
@@ -0,0 +1,169 @@
+require 'json'
+require 'socket'
+
+class IrkerWorker
+ include Sidekiq::Worker
+
+ def perform(project_id, chans, colors, push_data, settings)
+ project = Project.find(project_id)
+
+ # Get config parameters
+ return false unless init_perform settings, chans, colors
+
+ repo_name = push_data['repository']['name']
+ committer = push_data['user_name']
+ branch = push_data['ref'].gsub(%r'refs/[^/]*/', '')
+
+ if @colors
+ repo_name = "\x0304#{repo_name}\x0f"
+ branch = "\x0305#{branch}\x0f"
+ end
+
+ # Firsts messages are for branch creation/deletion
+ send_branch_updates push_data, project, repo_name, committer, branch
+
+ # Next messages are for commits
+ send_commits push_data, project, repo_name, committer, branch
+
+ close_connection
+ true
+ end
+
+ private
+
+ def init_perform(set, chans, colors)
+ @colors = colors
+ @channels = chans
+ start_connection set['server_ip'], set['server_port']
+ end
+
+ def start_connection(irker_server, irker_port)
+ begin
+ @socket = TCPSocket.new irker_server, irker_port
+ rescue Errno::ECONNREFUSED => e
+ logger.fatal "Can't connect to Irker daemon: #{e}"
+ return false
+ end
+ true
+ end
+
+ def sendtoirker(privmsg)
+ to_send = { to: @channels, privmsg: privmsg }
+ @socket.puts JSON.dump(to_send)
+ end
+
+ def close_connection
+ @socket.close
+ end
+
+ def send_branch_updates(push_data, project, repo_name, committer, branch)
+ if push_data['before'] =~ /^000000/
+ send_new_branch project, repo_name, committer, branch
+ elsif push_data['after'] =~ /^000000/
+ send_del_branch repo_name, committer, branch
+ end
+ end
+
+ def send_new_branch(project, repo_name, committer, branch)
+ repo_path = project.path_with_namespace
+ newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches"
+ newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors
+
+ privmsg = "[#{repo_name}] #{committer} has created a new branch "
+ privmsg += "#{branch}: #{newbranch}"
+ sendtoirker privmsg
+ end
+
+ def send_del_branch(repo_name, committer, branch)
+ privmsg = "[#{repo_name}] #{committer} has deleted the branch #{branch}"
+ sendtoirker privmsg
+ end
+
+ def send_commits(push_data, project, repo_name, committer, branch)
+ return if push_data['total_commits_count'] == 0
+
+ # Next message is for number of commit pushed, if any
+ if push_data['before'] =~ /^000000/
+ # Tweak on push_data["before"] in order to have a nice compare URL
+ push_data['before'] = before_on_new_branch push_data, project
+ end
+
+ send_commits_count(push_data, project, repo_name, committer, branch)
+
+ # One message per commit, limited by 3 messages (same limit as the
+ # github irc hook)
+ commits = push_data['commits'].first(3)
+ commits.each do |hook_attrs|
+ send_one_commit project, hook_attrs, repo_name, branch
+ end
+ end
+
+ def before_on_new_branch(push_data, project)
+ commit = commit_from_id project, push_data['commits'][0]['id']
+ parents = commit.parents
+ # Return old value if there's no new one
+ return push_data['before'] if parents.empty?
+ # Or return the first parent-commit
+ parents[0].id
+ end
+
+ def send_commits_count(data, project, repo, committer, branch)
+ url = compare_url data, project.path_with_namespace
+ commits = colorize_commits data['total_commits_count']
+
+ new_commits = 'new commit'
+ new_commits += 's' if data['total_commits_count'] > 1
+
+ sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \
+ "to #{branch}: #{url}"
+ end
+
+ def compare_url(data, repo_path)
+ sha1 = Commit::truncate_sha(data['before'])
+ sha2 = Commit::truncate_sha(data['after'])
+ compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare"
+ compare_url += "/#{sha1}...#{sha2}"
+ colorize_url compare_url
+ end
+
+ def send_one_commit(project, hook_attrs, repo_name, branch)
+ commit = commit_from_id project, hook_attrs['id']
+ sha = colorize_sha Commit::truncate_sha(hook_attrs['id'])
+ author = hook_attrs['author']['name']
+ files = colorize_nb_files(files_count commit)
+ title = commit.title
+
+ sendtoirker "#{repo_name}/#{branch} #{sha} #{author} (#{files}): #{title}"
+ end
+
+ def commit_from_id(project, id)
+ commit = Gitlab::Git::Commit.find(project.repository, id)
+ Commit.new(commit)
+ end
+
+ def files_count(commit)
+ files = "#{commit.diffs.count} file"
+ files += 's' if commit.diffs.count > 1
+ files
+ end
+
+ def colorize_sha(sha)
+ sha = "\x0314#{sha}\x0f" if @colors
+ sha
+ end
+
+ def colorize_nb_files(nb_files)
+ nb_files = "\x0312#{nb_files}\x0f" if @colors
+ nb_files
+ end
+
+ def colorize_url(url)
+ url = "\x0302\x1f#{url}\x0f" if @colors
+ url
+ end
+
+ def colorize_commits(commits)
+ commits = "\x02#{commits}\x0f" if @colors
+ commits
+ end
+end
diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md
new file mode 100644
index 00000000000..780a45bca20
--- /dev/null
+++ b/doc/project_services/irker.md
@@ -0,0 +1,46 @@
+# Irker IRC Gateway
+
+GitLab provides a way to push update messages to an Irker server. When
+configured, pushes to a project will trigger the service to send data directly
+to the Irker server.
+
+See the project homepage for further info: http://www.catb.org/esr/irker/
+
+## Needed setup
+
+You will first need an Irker daemon. You can download the Irker code from its
+gitorious repository on https://gitorious.org/irker: `git clone
+git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can
+run the python script named `irkerd`. This script is the gateway script, it acts
+both as an IRC client, for sending messages to an IRC server obviously, and as a
+TCP server, for receiving messages from the GitLab service.
+
+If the Irker server runs on the same machine, you are done. If not, you will
+need to follow the firsts steps of the next section.
+
+## Optional setup
+
+In the `app/models/project_services/irker_service.rb` file, you can modify some
+options in the `initialize_settings` method:
+- **server_ip** (defaults to `localhost`): the server IP address where the
+`irkerd` daemon runs;
+- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon;
+- **max_channels** (defaults to `3`): the maximum number of recipients the
+client is authorized to join, per project;
+- **default_irc_uri** (no default) : if this option is set, it has to be in the
+format `irc[s]://domain.name` and will be prepend to each and every channel
+provided by the user which is not a full URI.
+
+If the Irker server and the GitLab application do not run on the same host, you
+will **need** to setup at least the **server_ip** option.
+
+## Note on Irker recipients
+
+Irker accepts channel names of the form `chan` and `#chan`, both for the
+`#chan` channel. If you want to send messages in query, you will need to add
+`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter
+case, `Aorimn` is treated as a nick and no more as a channel name.
+
+Irker can also join password-protected channels. Users need to append
+`?key=thesecretpassword` to the chan name.
+
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 93a57485cfd..86eda341d6c 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -13,6 +13,7 @@ __Project integrations with external services for continuous integration and mor
- Gemnasium
- GitLab CI
- HipChat
+- [Irker](irker.md) An IRC gateway to receive messages on repository updates.
- Pivotal Tracker
- Pushover
- Slack
diff --git a/features/project/service.feature b/features/project/service.feature
index d0600aca010..fdff640ec85 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -61,6 +61,12 @@ Feature: Project Services
And I fill email on push settings
Then I should see email on push service settings saved
+ Scenario: Activate Irker (IRC Gateway) service
+ When I visit project "Shop" services page
+ And I click Irker service link
+ And I fill Irker settings
+ Then I should see Irker service settings saved
+
Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 3307117e69a..4b3d79324ab 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -17,6 +17,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
page.should have_content 'Atlassian Bamboo'
page.should have_content 'JetBrains TeamCity'
page.should have_content 'Asana'
+ page.should have_content 'Irker (IRC gateway)'
end
step 'I click gitlab-ci service link' do
@@ -132,6 +133,22 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
find_field('Recipients').value.should == 'qa@company.name'
end
+ step 'I click Irker service link' do
+ click_link 'Irker (IRC gateway)'
+ end
+
+ step 'I fill Irker settings' do
+ check 'Active'
+ fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits'
+ check 'Colorize messages'
+ click_button 'Save'
+ end
+
+ step 'I should see Irker service settings saved' do
+ find_field('Recipients').value.should == 'irc://chat.freenode.net/#commits'
+ find_field('Colorize messages').value.should == '1'
+ end
+
step 'I click Slack service link' do
click_link 'Slack'
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
new file mode 100644
index 00000000000..bbd5245ad34
--- /dev/null
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -0,0 +1,103 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+#
+
+require 'spec_helper'
+require 'socket'
+require 'json'
+
+describe IrkerService do
+ describe 'Associations' do
+ it { should belong_to :project }
+ it { should have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ before do
+ subject.active = true
+ subject.properties['recipients'] = _recipients
+ end
+
+ context 'active' do
+ let(:_recipients) { nil }
+ it { should validate_presence_of :recipients }
+ end
+
+ context 'too many recipients' do
+ let(:_recipients) { 'a b c d' }
+ it 'should add an error if there is too many recipients' do
+ subject.send :check_recipients_count
+ subject.errors.should_not be_blank
+ end
+ end
+
+ context '3 recipients' do
+ let(:_recipients) { 'a b c' }
+ it 'should not add an error if there is 3 recipients' do
+ subject.send :check_recipients_count
+ subject.errors.should be_blank
+ end
+ end
+ end
+
+ describe 'Execute' do
+ let(:irker) { IrkerService.new }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+
+ let(:recipients) { '#commits' }
+ let(:colorize_messages) { '1' }
+
+ before do
+ irker.stub(
+ active: true,
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ properties: {
+ 'recipients' => recipients,
+ 'colorize_messages' => colorize_messages
+ }
+ )
+ irker.settings = {
+ server_ip: 'localhost',
+ server_port: 6659,
+ max_channels: 3,
+ default_irc_uri: 'irc://chat.freenode.net/'
+ }
+ irker.valid?
+ @irker_server = TCPServer.new 'localhost', 6659
+ end
+
+ after do
+ @irker_server.close
+ end
+
+ it 'should send valid JSON messages to an Irker listener' do
+ irker.execute(sample_data)
+
+ conn = @irker_server.accept
+ conn.readlines.each do |line|
+ msg = JSON.load(line.chomp("\n"))
+ msg.keys.should match_array(['to', 'privmsg'])
+ if msg['to'].is_a?(String)
+ msg['to'].should == 'irc://chat.freenode.net/#commits'
+ else
+ msg['to'].should match_array(['irc://chat.freenode.net/#commits'])
+ end
+ end
+ conn.close
+ end
+ end
+end