diff --git a/CHANGELOG b/CHANGELOG
index dae32953cd93ecb7c465a22703bcf7deb0623714..ee862a4ca3cd14e7d779c7de2a4a1b5f71252c8d 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 5c29a6550f55d2e05f472290f985aa2acf79e77d..e7823020e6043cdffcd4808d669ddfd4fd1da6ed 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 7f2e0b4c17b44b8e232c5c6dcc64c0690bb9e5bf..907f331d8f1e7315e8e612659124b117b21a3823 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 0000000000000000000000000000000000000000..a0203a5bb107f94d3df9d90e7e91e7f7b25bfc03
--- /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 f87d875c10a7e2d77723b77ccfbd4fbdca833773..f4e97da321203712290aa7b2cb421d1f9d0f78fb 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 0000000000000000000000000000000000000000..613bae351d8e854ab501e2cfac2f9842b8d64f01
--- /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 0000000000000000000000000000000000000000..780a45bca20fbc5d533ec17f358f077337192f34
--- /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 93a57485cfd3a8e346ae0c33f7448b52d062c16c..86eda341d6ca3c6cac1a2eabca79783547a997c5 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 d0600aca010e14f5af75b938311de75e81770673..fdff640ec857e1ea267fbf77af7d3312a266c2d7 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 3307117e69a1f595f57fa926b2b3b820d753f532..4b3d79324ab62facffa21048e6f2165a3c4b09c5 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 0000000000000000000000000000000000000000..bbd5245ad34beaa7a5024c99d02a59be9a1daac0
--- /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