Commit afd747d4d7af9e4f5387927bcaa709a2d3fefbef
Exists in
master
and in
4 other branches
Merge branch 'feature/email_on_push_service' of /home/git/repositories/gitlab/gitlabhq
Showing
26 changed files
with
587 additions
and
413 deletions
Show diff stats
app/mailers/emails/projects.rb
| ... | ... | @@ -13,5 +13,15 @@ module Emails |
| 13 | 13 | mail(to: @user.email, |
| 14 | 14 | subject: subject("Project was moved")) |
| 15 | 15 | end |
| 16 | + | |
| 17 | + def repository_push_email(project_id, recipient, author_id, branch, compare) | |
| 18 | + @project = Project.find(project_id) | |
| 19 | + @author = User.find(author_id) | |
| 20 | + @commits = Commit.decorate(compare.commits) | |
| 21 | + @diffs = compare.diffs | |
| 22 | + @branch = branch | |
| 23 | + | |
| 24 | + mail(to: recipient, subject: subject("New push to repository")) | |
| 25 | + end | |
| 16 | 26 | end |
| 17 | 27 | end | ... | ... |
app/models/assembla_service.rb
| ... | ... | @@ -1,45 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -class AssemblaService < Service | |
| 19 | - include HTTParty | |
| 20 | - | |
| 21 | - validates :token, presence: true, if: :activated? | |
| 22 | - | |
| 23 | - def title | |
| 24 | - 'Assembla' | |
| 25 | - end | |
| 26 | - | |
| 27 | - def description | |
| 28 | - 'Project Management Software (Source Commits Endpoint)' | |
| 29 | - end | |
| 30 | - | |
| 31 | - def to_param | |
| 32 | - 'assembla' | |
| 33 | - end | |
| 34 | - | |
| 35 | - def fields | |
| 36 | - [ | |
| 37 | - { type: 'text', name: 'token', placeholder: '' } | |
| 38 | - ] | |
| 39 | - end | |
| 40 | - | |
| 41 | - def execute(push) | |
| 42 | - url = "https://atlas.assembla.com/spaces/ouposp/github_tool?secret_key=#{token}" | |
| 43 | - AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' }) | |
| 44 | - end | |
| 45 | -end |
app/models/campfire_service.rb
| ... | ... | @@ -1,78 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -class CampfireService < Service | |
| 19 | - attr_accessible :subdomain, :room | |
| 20 | - | |
| 21 | - validates :token, presence: true, if: :activated? | |
| 22 | - | |
| 23 | - def title | |
| 24 | - 'Campfire' | |
| 25 | - end | |
| 26 | - | |
| 27 | - def description | |
| 28 | - 'Simple web-based real-time group chat' | |
| 29 | - end | |
| 30 | - | |
| 31 | - def to_param | |
| 32 | - 'campfire' | |
| 33 | - end | |
| 34 | - | |
| 35 | - def fields | |
| 36 | - [ | |
| 37 | - { type: 'text', name: 'token', placeholder: '' }, | |
| 38 | - { type: 'text', name: 'subdomain', placeholder: '' }, | |
| 39 | - { type: 'text', name: 'room', placeholder: '' } | |
| 40 | - ] | |
| 41 | - end | |
| 42 | - | |
| 43 | - def execute(push_data) | |
| 44 | - room = gate.find_room_by_name(self.room) | |
| 45 | - return true unless room | |
| 46 | - | |
| 47 | - message = build_message(push_data) | |
| 48 | - | |
| 49 | - room.speak(message) | |
| 50 | - end | |
| 51 | - | |
| 52 | - private | |
| 53 | - | |
| 54 | - def gate | |
| 55 | - @gate ||= Tinder::Campfire.new(subdomain, token: token) | |
| 56 | - end | |
| 57 | - | |
| 58 | - def build_message(push) | |
| 59 | - ref = push[:ref].gsub("refs/heads/", "") | |
| 60 | - before = push[:before] | |
| 61 | - after = push[:after] | |
| 62 | - | |
| 63 | - message = "" | |
| 64 | - message << "[#{project.name_with_namespace}] " | |
| 65 | - message << "#{push[:user_name]} " | |
| 66 | - | |
| 67 | - if before =~ /000000/ | |
| 68 | - message << "pushed new branch #{ref} \n" | |
| 69 | - elsif after =~ /000000/ | |
| 70 | - message << "removed branch #{ref} \n" | |
| 71 | - else | |
| 72 | - message << "pushed #{push[:total_commits_count]} commits to #{ref}. " | |
| 73 | - message << "#{project.web_url}/compare/#{before}...#{after}" | |
| 74 | - end | |
| 75 | - | |
| 76 | - message | |
| 77 | - end | |
| 78 | -end |
app/models/flowdock_service.rb
| ... | ... | @@ -1,54 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -require "flowdock-git-hook" | |
| 19 | - | |
| 20 | -class FlowdockService < Service | |
| 21 | - validates :token, presence: true, if: :activated? | |
| 22 | - | |
| 23 | - def title | |
| 24 | - 'Flowdock' | |
| 25 | - end | |
| 26 | - | |
| 27 | - def description | |
| 28 | - 'Flowdock is a collaboration web app for technical teams.' | |
| 29 | - end | |
| 30 | - | |
| 31 | - def to_param | |
| 32 | - 'flowdock' | |
| 33 | - end | |
| 34 | - | |
| 35 | - def fields | |
| 36 | - [ | |
| 37 | - { type: 'text', name: 'token', placeholder: '' } | |
| 38 | - ] | |
| 39 | - end | |
| 40 | - | |
| 41 | - def execute(push_data) | |
| 42 | - repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") | |
| 43 | - Flowdock::Git.post( | |
| 44 | - push_data[:ref], | |
| 45 | - push_data[:before], | |
| 46 | - push_data[:after], | |
| 47 | - token: token, | |
| 48 | - repo: repo_path, | |
| 49 | - repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", | |
| 50 | - commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", | |
| 51 | - diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", | |
| 52 | - ) | |
| 53 | - end | |
| 54 | -end |
app/models/gitlab_ci_service.rb
| ... | ... | @@ -1,78 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -class GitlabCiService < Service | |
| 19 | - attr_accessible :project_url | |
| 20 | - | |
| 21 | - validates :project_url, presence: true, if: :activated? | |
| 22 | - validates :token, presence: true, if: :activated? | |
| 23 | - | |
| 24 | - delegate :execute, to: :service_hook, prefix: nil | |
| 25 | - | |
| 26 | - after_save :compose_service_hook, if: :activated? | |
| 27 | - | |
| 28 | - def compose_service_hook | |
| 29 | - hook = service_hook || build_service_hook | |
| 30 | - hook.url = [project_url, "/build", "?token=#{token}"].join("") | |
| 31 | - hook.save | |
| 32 | - end | |
| 33 | - | |
| 34 | - def commit_status_path sha | |
| 35 | - project_url + "/builds/#{sha}/status.json?token=#{token}" | |
| 36 | - end | |
| 37 | - | |
| 38 | - def commit_status sha | |
| 39 | - response = HTTParty.get(commit_status_path(sha)) | |
| 40 | - | |
| 41 | - if response.code == 200 and response["status"] | |
| 42 | - response["status"] | |
| 43 | - else | |
| 44 | - :error | |
| 45 | - end | |
| 46 | - end | |
| 47 | - | |
| 48 | - def build_page sha | |
| 49 | - project_url + "/builds/#{sha}" | |
| 50 | - end | |
| 51 | - | |
| 52 | - def builds_path | |
| 53 | - project_url + "?ref=" + project.default_branch | |
| 54 | - end | |
| 55 | - | |
| 56 | - def status_img_path | |
| 57 | - project_url + "/status.png?ref=" + project.default_branch | |
| 58 | - end | |
| 59 | - | |
| 60 | - def title | |
| 61 | - 'GitLab CI' | |
| 62 | - end | |
| 63 | - | |
| 64 | - def description | |
| 65 | - 'Continuous integration server from GitLab' | |
| 66 | - end | |
| 67 | - | |
| 68 | - def to_param | |
| 69 | - 'gitlab_ci' | |
| 70 | - end | |
| 71 | - | |
| 72 | - def fields | |
| 73 | - [ | |
| 74 | - { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' }, | |
| 75 | - { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3'} | |
| 76 | - ] | |
| 77 | - end | |
| 78 | -end |
app/models/hipchat_service.rb
| ... | ... | @@ -1,74 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -class HipchatService < Service | |
| 19 | - attr_accessible :room | |
| 20 | - | |
| 21 | - validates :token, presence: true, if: :activated? | |
| 22 | - | |
| 23 | - def title | |
| 24 | - 'Hipchat' | |
| 25 | - end | |
| 26 | - | |
| 27 | - def description | |
| 28 | - 'Private group chat and IM' | |
| 29 | - end | |
| 30 | - | |
| 31 | - def to_param | |
| 32 | - 'hipchat' | |
| 33 | - end | |
| 34 | - | |
| 35 | - def fields | |
| 36 | - [ | |
| 37 | - { type: 'text', name: 'token', placeholder: '' }, | |
| 38 | - { type: 'text', name: 'room', placeholder: '' } | |
| 39 | - ] | |
| 40 | - end | |
| 41 | - | |
| 42 | - def execute(push_data) | |
| 43 | - gate[room].send('Gitlab', create_message(push_data)) | |
| 44 | - end | |
| 45 | - | |
| 46 | - private | |
| 47 | - | |
| 48 | - def gate | |
| 49 | - @gate ||= HipChat::Client.new(token) | |
| 50 | - end | |
| 51 | - | |
| 52 | - def create_message(push) | |
| 53 | - ref = push[:ref].gsub("refs/heads/", "") | |
| 54 | - before = push[:before] | |
| 55 | - after = push[:after] | |
| 56 | - | |
| 57 | - message = "" | |
| 58 | - message << "#{push[:user_name]} " | |
| 59 | - if before =~ /000000/ | |
| 60 | - message << "pushed new branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> to <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a>\n" | |
| 61 | - elsif after =~ /000000/ | |
| 62 | - message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n" | |
| 63 | - else | |
| 64 | - message << "#pushed to branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> " | |
| 65 | - message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> " | |
| 66 | - message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" | |
| 67 | - for commit in push[:commits] do | |
| 68 | - message << "<br /> - #{commit[:message]} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" | |
| 69 | - end | |
| 70 | - end | |
| 71 | - | |
| 72 | - message | |
| 73 | - end | |
| 74 | -end |
app/models/pivotaltracker_service.rb
| ... | ... | @@ -1,62 +0,0 @@ |
| 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 | -# | |
| 17 | - | |
| 18 | -class PivotaltrackerService < Service | |
| 19 | - include HTTParty | |
| 20 | - | |
| 21 | - validates :token, presence: true, if: :activated? | |
| 22 | - | |
| 23 | - def title | |
| 24 | - 'PivotalTracker' | |
| 25 | - end | |
| 26 | - | |
| 27 | - def description | |
| 28 | - 'Project Management Software (Source Commits Endpoint)' | |
| 29 | - end | |
| 30 | - | |
| 31 | - def to_param | |
| 32 | - 'pivotaltracker' | |
| 33 | - end | |
| 34 | - | |
| 35 | - def fields | |
| 36 | - [ | |
| 37 | - { type: 'text', name: 'token', placeholder: '' } | |
| 38 | - ] | |
| 39 | - end | |
| 40 | - | |
| 41 | - def execute(push) | |
| 42 | - url = 'https://www.pivotaltracker.com/services/v5/source_commits' | |
| 43 | - push[:commits].each do |commit| | |
| 44 | - message = { | |
| 45 | - 'source_commit' => { | |
| 46 | - 'commit_id' => commit[:id], | |
| 47 | - 'author' => commit[:author][:name], | |
| 48 | - 'url' => commit[:url], | |
| 49 | - 'message' => commit[:message] | |
| 50 | - } | |
| 51 | - } | |
| 52 | - PivotaltrackerService.post( | |
| 53 | - url, | |
| 54 | - body: message.to_json, | |
| 55 | - headers: { | |
| 56 | - 'Content-Type' => 'application/json', | |
| 57 | - 'X-TrackerToken' => token | |
| 58 | - } | |
| 59 | - ) | |
| 60 | - end | |
| 61 | - end | |
| 62 | -end |
app/models/project.rb
| ... | ... | @@ -48,6 +48,7 @@ class Project < ActiveRecord::Base |
| 48 | 48 | has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' |
| 49 | 49 | has_one :gitlab_ci_service, dependent: :destroy |
| 50 | 50 | has_one :campfire_service, dependent: :destroy |
| 51 | + has_one :emails_on_push_service, dependent: :destroy | |
| 51 | 52 | has_one :pivotaltracker_service, dependent: :destroy |
| 52 | 53 | has_one :hipchat_service, dependent: :destroy |
| 53 | 54 | has_one :flowdock_service, dependent: :destroy |
| ... | ... | @@ -237,7 +238,7 @@ class Project < ActiveRecord::Base |
| 237 | 238 | end |
| 238 | 239 | |
| 239 | 240 | def available_services_names |
| 240 | - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla) | |
| 241 | + %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla emails_on_push) | |
| 241 | 242 | end |
| 242 | 243 | |
| 243 | 244 | def gitlab_ci? | ... | ... |
| ... | ... | @@ -0,0 +1,45 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class AssemblaService < Service | |
| 19 | + include HTTParty | |
| 20 | + | |
| 21 | + validates :token, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'Assembla' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Project Management Software (Source Commits Endpoint)' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'assembla' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def fields | |
| 36 | + [ | |
| 37 | + { type: 'text', name: 'token', placeholder: '' } | |
| 38 | + ] | |
| 39 | + end | |
| 40 | + | |
| 41 | + def execute(push) | |
| 42 | + url = "https://atlas.assembla.com/spaces/ouposp/github_tool?secret_key=#{token}" | |
| 43 | + AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' }) | |
| 44 | + end | |
| 45 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,78 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class CampfireService < Service | |
| 19 | + attr_accessible :subdomain, :room | |
| 20 | + | |
| 21 | + validates :token, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'Campfire' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Simple web-based real-time group chat' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'campfire' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def fields | |
| 36 | + [ | |
| 37 | + { type: 'text', name: 'token', placeholder: '' }, | |
| 38 | + { type: 'text', name: 'subdomain', placeholder: '' }, | |
| 39 | + { type: 'text', name: 'room', placeholder: '' } | |
| 40 | + ] | |
| 41 | + end | |
| 42 | + | |
| 43 | + def execute(push_data) | |
| 44 | + room = gate.find_room_by_name(self.room) | |
| 45 | + return true unless room | |
| 46 | + | |
| 47 | + message = build_message(push_data) | |
| 48 | + | |
| 49 | + room.speak(message) | |
| 50 | + end | |
| 51 | + | |
| 52 | + private | |
| 53 | + | |
| 54 | + def gate | |
| 55 | + @gate ||= Tinder::Campfire.new(subdomain, token: token) | |
| 56 | + end | |
| 57 | + | |
| 58 | + def build_message(push) | |
| 59 | + ref = push[:ref].gsub("refs/heads/", "") | |
| 60 | + before = push[:before] | |
| 61 | + after = push[:after] | |
| 62 | + | |
| 63 | + message = "" | |
| 64 | + message << "[#{project.name_with_namespace}] " | |
| 65 | + message << "#{push[:user_name]} " | |
| 66 | + | |
| 67 | + if before =~ /000000/ | |
| 68 | + message << "pushed new branch #{ref} \n" | |
| 69 | + elsif after =~ /000000/ | |
| 70 | + message << "removed branch #{ref} \n" | |
| 71 | + else | |
| 72 | + message << "pushed #{push[:total_commits_count]} commits to #{ref}. " | |
| 73 | + message << "#{project.web_url}/compare/#{before}...#{after}" | |
| 74 | + end | |
| 75 | + | |
| 76 | + message | |
| 77 | + end | |
| 78 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,44 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class EmailsOnPushService < Service | |
| 19 | + attr_accessible :recipients | |
| 20 | + | |
| 21 | + validates :recipients, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'Emails on push' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Email the commits and diff of each push to a list of recipients.' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'emails_on_push' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def execute(push_data) | |
| 36 | + EmailsOnPushWorker.perform_async(project_id, recipients, push_data) | |
| 37 | + end | |
| 38 | + | |
| 39 | + def fields | |
| 40 | + [ | |
| 41 | + { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, | |
| 42 | + ] | |
| 43 | + end | |
| 44 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,54 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +require "flowdock-git-hook" | |
| 19 | + | |
| 20 | +class FlowdockService < Service | |
| 21 | + validates :token, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'Flowdock' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Flowdock is a collaboration web app for technical teams.' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'flowdock' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def fields | |
| 36 | + [ | |
| 37 | + { type: 'text', name: 'token', placeholder: '' } | |
| 38 | + ] | |
| 39 | + end | |
| 40 | + | |
| 41 | + def execute(push_data) | |
| 42 | + repo_path = File.join(Gitlab.config.gitlab_shell.repos_path, "#{project.path_with_namespace}.git") | |
| 43 | + Flowdock::Git.post( | |
| 44 | + push_data[:ref], | |
| 45 | + push_data[:before], | |
| 46 | + push_data[:after], | |
| 47 | + token: token, | |
| 48 | + repo: repo_path, | |
| 49 | + repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", | |
| 50 | + commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s", | |
| 51 | + diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s", | |
| 52 | + ) | |
| 53 | + end | |
| 54 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,78 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class GitlabCiService < Service | |
| 19 | + attr_accessible :project_url | |
| 20 | + | |
| 21 | + validates :project_url, presence: true, if: :activated? | |
| 22 | + validates :token, presence: true, if: :activated? | |
| 23 | + | |
| 24 | + delegate :execute, to: :service_hook, prefix: nil | |
| 25 | + | |
| 26 | + after_save :compose_service_hook, if: :activated? | |
| 27 | + | |
| 28 | + def compose_service_hook | |
| 29 | + hook = service_hook || build_service_hook | |
| 30 | + hook.url = [project_url, "/build", "?token=#{token}"].join("") | |
| 31 | + hook.save | |
| 32 | + end | |
| 33 | + | |
| 34 | + def commit_status_path sha | |
| 35 | + project_url + "/builds/#{sha}/status.json?token=#{token}" | |
| 36 | + end | |
| 37 | + | |
| 38 | + def commit_status sha | |
| 39 | + response = HTTParty.get(commit_status_path(sha)) | |
| 40 | + | |
| 41 | + if response.code == 200 and response["status"] | |
| 42 | + response["status"] | |
| 43 | + else | |
| 44 | + :error | |
| 45 | + end | |
| 46 | + end | |
| 47 | + | |
| 48 | + def build_page sha | |
| 49 | + project_url + "/builds/#{sha}" | |
| 50 | + end | |
| 51 | + | |
| 52 | + def builds_path | |
| 53 | + project_url + "?ref=" + project.default_branch | |
| 54 | + end | |
| 55 | + | |
| 56 | + def status_img_path | |
| 57 | + project_url + "/status.png?ref=" + project.default_branch | |
| 58 | + end | |
| 59 | + | |
| 60 | + def title | |
| 61 | + 'GitLab CI' | |
| 62 | + end | |
| 63 | + | |
| 64 | + def description | |
| 65 | + 'Continuous integration server from GitLab' | |
| 66 | + end | |
| 67 | + | |
| 68 | + def to_param | |
| 69 | + 'gitlab_ci' | |
| 70 | + end | |
| 71 | + | |
| 72 | + def fields | |
| 73 | + [ | |
| 74 | + { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' }, | |
| 75 | + { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3'} | |
| 76 | + ] | |
| 77 | + end | |
| 78 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,74 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class HipchatService < Service | |
| 19 | + attr_accessible :room | |
| 20 | + | |
| 21 | + validates :token, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'Hipchat' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Private group chat and IM' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'hipchat' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def fields | |
| 36 | + [ | |
| 37 | + { type: 'text', name: 'token', placeholder: '' }, | |
| 38 | + { type: 'text', name: 'room', placeholder: '' } | |
| 39 | + ] | |
| 40 | + end | |
| 41 | + | |
| 42 | + def execute(push_data) | |
| 43 | + gate[room].send('Gitlab', create_message(push_data)) | |
| 44 | + end | |
| 45 | + | |
| 46 | + private | |
| 47 | + | |
| 48 | + def gate | |
| 49 | + @gate ||= HipChat::Client.new(token) | |
| 50 | + end | |
| 51 | + | |
| 52 | + def create_message(push) | |
| 53 | + ref = push[:ref].gsub("refs/heads/", "") | |
| 54 | + before = push[:before] | |
| 55 | + after = push[:after] | |
| 56 | + | |
| 57 | + message = "" | |
| 58 | + message << "#{push[:user_name]} " | |
| 59 | + if before =~ /000000/ | |
| 60 | + message << "pushed new branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> to <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a>\n" | |
| 61 | + elsif after =~ /000000/ | |
| 62 | + message << "removed branch #{ref} from <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> \n" | |
| 63 | + else | |
| 64 | + message << "#pushed to branch <a href=\"#{project.web_url}/commits/#{ref}\">#{ref}</a> " | |
| 65 | + message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> " | |
| 66 | + message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" | |
| 67 | + for commit in push[:commits] do | |
| 68 | + message << "<br /> - #{commit[:message]} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" | |
| 69 | + end | |
| 70 | + end | |
| 71 | + | |
| 72 | + message | |
| 73 | + end | |
| 74 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,62 @@ |
| 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 | +# | |
| 17 | + | |
| 18 | +class PivotaltrackerService < Service | |
| 19 | + include HTTParty | |
| 20 | + | |
| 21 | + validates :token, presence: true, if: :activated? | |
| 22 | + | |
| 23 | + def title | |
| 24 | + 'PivotalTracker' | |
| 25 | + end | |
| 26 | + | |
| 27 | + def description | |
| 28 | + 'Project Management Software (Source Commits Endpoint)' | |
| 29 | + end | |
| 30 | + | |
| 31 | + def to_param | |
| 32 | + 'pivotaltracker' | |
| 33 | + end | |
| 34 | + | |
| 35 | + def fields | |
| 36 | + [ | |
| 37 | + { type: 'text', name: 'token', placeholder: '' } | |
| 38 | + ] | |
| 39 | + end | |
| 40 | + | |
| 41 | + def execute(push) | |
| 42 | + url = 'https://www.pivotaltracker.com/services/v5/source_commits' | |
| 43 | + push[:commits].each do |commit| | |
| 44 | + message = { | |
| 45 | + 'source_commit' => { | |
| 46 | + 'commit_id' => commit[:id], | |
| 47 | + 'author' => commit[:author][:name], | |
| 48 | + 'url' => commit[:url], | |
| 49 | + 'message' => commit[:message] | |
| 50 | + } | |
| 51 | + } | |
| 52 | + PivotaltrackerService.post( | |
| 53 | + url, | |
| 54 | + body: message.to_json, | |
| 55 | + headers: { | |
| 56 | + 'Content-Type' => 'application/json', | |
| 57 | + 'X-TrackerToken' => token | |
| 58 | + } | |
| 59 | + ) | |
| 60 | + end | |
| 61 | + end | |
| 62 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | +%h3 #{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} | |
| 2 | + | |
| 3 | +%h4 Commits: | |
| 4 | + | |
| 5 | +%ul | |
| 6 | + - @commits.each do |commit| | |
| 7 | + %li | |
| 8 | + #{commit.short_id} - #{commit.title} | |
| 9 | + | |
| 10 | +%h4 Diff: | |
| 11 | +- @diffs.each do |diff| | |
| 12 | + %li | |
| 13 | + %strong | |
| 14 | + - if diff.old_path == diff.new_path | |
| 15 | + = diff.new_path | |
| 16 | + - elsif diff.new_path && diff.old_path | |
| 17 | + #{diff.old_path} → #{diff.new_path} | |
| 18 | + - else | |
| 19 | + = diff.new_path || diff.old_path | |
| 20 | + %hr | |
| 21 | + %pre | |
| 22 | + = diff.diff | |
| 23 | + %br | ... | ... |
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} | |
| 2 | + | |
| 3 | +\ | |
| 4 | +Commits: | |
| 5 | +- @commits.each do |commit| | |
| 6 | + #{commit.short_id} - #{truncate(commit.title, length: 40)} | |
| 7 | +\ | |
| 8 | +\ | |
| 9 | +Diff: | |
| 10 | +- @diffs.each do |diff| | |
| 11 | + \ | |
| 12 | + \===================================== | |
| 13 | + - if diff.old_path == diff.new_path | |
| 14 | + = diff.new_path | |
| 15 | + - elsif diff.new_path && diff.old_path | |
| 16 | + #{diff.old_path} → #{diff.new_path} | |
| 17 | + - else | |
| 18 | + = diff.new_path || diff.old_path | |
| 19 | + \===================================== | |
| 20 | + = diff.diff | ... | ... |
app/views/projects/services/_form.html.haml
| ... | ... | @@ -33,6 +33,8 @@ |
| 33 | 33 | .controls |
| 34 | 34 | - if type == 'text' |
| 35 | 35 | = f.text_field name, class: "input-xlarge", placeholder: placeholder |
| 36 | + - elsif type == 'textarea' | |
| 37 | + = f.text_area name, rows: 5, class: "input-xxlarge", placeholder: placeholder | |
| 36 | 38 | - elsif type == 'checkbox' |
| 37 | 39 | = f.check_box name |
| 38 | 40 | ... | ... |
app/views/projects/services/index.html.haml
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +class EmailsOnPushWorker | |
| 2 | + include Sidekiq::Worker | |
| 3 | + | |
| 4 | + def perform(project_id, recipients, push_data) | |
| 5 | + project = Project.find(project_id) | |
| 6 | + before_sha = push_data["before"] | |
| 7 | + after_sha = push_data["after"] | |
| 8 | + branch = push_data["ref"] | |
| 9 | + author_id = push_data["user_id"] | |
| 10 | + | |
| 11 | + if before_sha =~ /^000000/ || after_sha =~ /^000000/ | |
| 12 | + # skip if new branch was pushed or branch was removed | |
| 13 | + return true | |
| 14 | + end | |
| 15 | + | |
| 16 | + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) | |
| 17 | + | |
| 18 | + # Do not send emails if git compare failed | |
| 19 | + return false unless compare && compare.commits.present? | |
| 20 | + | |
| 21 | + recipients.split(" ").each do |recipient| | |
| 22 | + Notify.delay.repository_push_email(project_id, recipient, author_id, branch, compare) | |
| 23 | + end | |
| 24 | + end | |
| 25 | +end | ... | ... |
config/application.rb
| ... | ... | @@ -12,7 +12,7 @@ module Gitlab |
| 12 | 12 | # -- all .rb files in that directory are automatically loaded. |
| 13 | 13 | |
| 14 | 14 | # Custom directories with classes and modules you want to be autoloadable. |
| 15 | - config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/models/concerns) | |
| 15 | + config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/models/concerns #{config.root}/app/models/project_services) | |
| 16 | 16 | |
| 17 | 17 | # Only load the plugins named here, in the order given (default is alphabetical). |
| 18 | 18 | # :all can be used as a placeholder for all plugins not explicitly named. | ... | ... |
db/schema.rb
| ... | ... | @@ -11,7 +11,7 @@ |
| 11 | 11 | # |
| 12 | 12 | # It's strongly recommended that you check this file into your version control system. |
| 13 | 13 | |
| 14 | -ActiveRecord::Schema.define(version: 20131214224427) do | |
| 14 | +ActiveRecord::Schema.define(version: 20131217102743) do | |
| 15 | 15 | |
| 16 | 16 | create_table "broadcast_messages", force: true do |t| |
| 17 | 17 | t.text "message", null: false |
| ... | ... | @@ -219,6 +219,7 @@ ActiveRecord::Schema.define(version: 20131214224427) do |
| 219 | 219 | t.string "project_url" |
| 220 | 220 | t.string "subdomain" |
| 221 | 221 | t.string "room" |
| 222 | + t.text "recipients" | |
| 222 | 223 | end |
| 223 | 224 | |
| 224 | 225 | add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree | ... | ... |
features/project/service.feature
| ... | ... | @@ -35,4 +35,10 @@ Feature: Project Services |
| 35 | 35 | When I visit project "Shop" services page |
| 36 | 36 | And I click Assembla service link |
| 37 | 37 | And I fill Assembla settings |
| 38 | - Then I should see Assembla service settings saved | |
| 39 | 38 | \ No newline at end of file |
| 39 | + Then I should see Assembla service settings saved | |
| 40 | + | |
| 41 | + Scenario: Activate email on push service | |
| 42 | + When I visit project "Shop" services page | |
| 43 | + And I click email on push service link | |
| 44 | + And I fill email on push settings | |
| 45 | + Then I should see email on push service settings saved | ... | ... |
features/steps/project/project_services.rb
| ... | ... | @@ -3,11 +3,11 @@ class ProjectServices < Spinach::FeatureSteps |
| 3 | 3 | include SharedProject |
| 4 | 4 | include SharedPaths |
| 5 | 5 | |
| 6 | - When 'I visit project "Shop" services page' do | |
| 6 | + step 'I visit project "Shop" services page' do | |
| 7 | 7 | visit project_services_path(@project) |
| 8 | 8 | end |
| 9 | 9 | |
| 10 | - Then 'I should see list of available services' do | |
| 10 | + step 'I should see list of available services' do | |
| 11 | 11 | page.should have_content 'Services' |
| 12 | 12 | page.should have_content 'Campfire' |
| 13 | 13 | page.should have_content 'Hipchat' |
| ... | ... | @@ -15,76 +15,89 @@ class ProjectServices < Spinach::FeatureSteps |
| 15 | 15 | page.should have_content 'Assembla' |
| 16 | 16 | end |
| 17 | 17 | |
| 18 | - And 'I click gitlab-ci service link' do | |
| 18 | + step 'I click gitlab-ci service link' do | |
| 19 | 19 | click_link 'GitLab CI' |
| 20 | 20 | end |
| 21 | 21 | |
| 22 | - And 'I fill gitlab-ci settings' do | |
| 22 | + step 'I fill gitlab-ci settings' do | |
| 23 | 23 | check 'Active' |
| 24 | 24 | fill_in 'Project url', with: 'http://ci.gitlab.org/projects/3' |
| 25 | 25 | fill_in 'Token', with: 'verySecret' |
| 26 | 26 | click_button 'Save' |
| 27 | 27 | end |
| 28 | 28 | |
| 29 | - Then 'I should see service settings saved' do | |
| 29 | + step 'I should see service settings saved' do | |
| 30 | 30 | find_field('Project url').value.should == 'http://ci.gitlab.org/projects/3' |
| 31 | 31 | end |
| 32 | 32 | |
| 33 | - And 'I click hipchat service link' do | |
| 33 | + step 'I click hipchat service link' do | |
| 34 | 34 | click_link 'Hipchat' |
| 35 | 35 | end |
| 36 | 36 | |
| 37 | - And 'I fill hipchat settings' do | |
| 37 | + step 'I fill hipchat settings' do | |
| 38 | 38 | check 'Active' |
| 39 | 39 | fill_in 'Room', with: 'gitlab' |
| 40 | 40 | fill_in 'Token', with: 'verySecret' |
| 41 | 41 | click_button 'Save' |
| 42 | 42 | end |
| 43 | 43 | |
| 44 | - Then 'I should see hipchat service settings saved' do | |
| 44 | + step 'I should see hipchat service settings saved' do | |
| 45 | 45 | find_field('Room').value.should == 'gitlab' |
| 46 | 46 | end |
| 47 | 47 | |
| 48 | 48 | |
| 49 | - And 'I click pivotaltracker service link' do | |
| 49 | + step 'I click pivotaltracker service link' do | |
| 50 | 50 | click_link 'PivotalTracker' |
| 51 | 51 | end |
| 52 | 52 | |
| 53 | - And 'I fill pivotaltracker settings' do | |
| 53 | + step 'I fill pivotaltracker settings' do | |
| 54 | 54 | check 'Active' |
| 55 | 55 | fill_in 'Token', with: 'verySecret' |
| 56 | 56 | click_button 'Save' |
| 57 | 57 | end |
| 58 | 58 | |
| 59 | - Then 'I should see pivotaltracker service settings saved' do | |
| 59 | + step 'I should see pivotaltracker service settings saved' do | |
| 60 | 60 | find_field('Token').value.should == 'verySecret' |
| 61 | 61 | end |
| 62 | 62 | |
| 63 | - And 'I click Flowdock service link' do | |
| 63 | + step 'I click Flowdock service link' do | |
| 64 | 64 | click_link 'Flowdock' |
| 65 | 65 | end |
| 66 | 66 | |
| 67 | - And 'I fill Flowdock settings' do | |
| 67 | + step 'I fill Flowdock settings' do | |
| 68 | 68 | check 'Active' |
| 69 | 69 | fill_in 'Token', with: 'verySecret' |
| 70 | 70 | click_button 'Save' |
| 71 | 71 | end |
| 72 | 72 | |
| 73 | - Then 'I should see Flowdock service settings saved' do | |
| 73 | + step 'I should see Flowdock service settings saved' do | |
| 74 | 74 | find_field('Token').value.should == 'verySecret' |
| 75 | 75 | end |
| 76 | 76 | |
| 77 | - And 'I click Assembla service link' do | |
| 77 | + step 'I click Assembla service link' do | |
| 78 | 78 | click_link 'Assembla' |
| 79 | 79 | end |
| 80 | 80 | |
| 81 | - And 'I fill Assembla settings' do | |
| 81 | + step 'I fill Assembla settings' do | |
| 82 | 82 | check 'Active' |
| 83 | 83 | fill_in 'Token', with: 'verySecret' |
| 84 | 84 | click_button 'Save' |
| 85 | 85 | end |
| 86 | 86 | |
| 87 | - Then 'I should see Assembla service settings saved' do | |
| 87 | + step 'I should see Assembla service settings saved' do | |
| 88 | 88 | find_field('Token').value.should == 'verySecret' |
| 89 | 89 | end |
| 90 | + | |
| 91 | + step 'I click email on push service link' do | |
| 92 | + click_link 'Emails on push' | |
| 93 | + end | |
| 94 | + | |
| 95 | + step 'I fill email on push settings' do | |
| 96 | + fill_in 'Recipients', with: 'qa@company.name' | |
| 97 | + click_button 'Save' | |
| 98 | + end | |
| 99 | + | |
| 100 | + step 'I should see email on push service settings saved' do | |
| 101 | + find_field('Recipients').value.should == 'qa@company.name' | |
| 102 | + end | |
| 90 | 103 | end | ... | ... |
spec/mailers/notify_spec.rb
| ... | ... | @@ -391,4 +391,28 @@ describe Notify do |
| 391 | 391 | should have_body_text /#{example_site_path}/ |
| 392 | 392 | end |
| 393 | 393 | end |
| 394 | + | |
| 395 | + describe 'email on push' do | |
| 396 | + let(:example_site_path) { root_path } | |
| 397 | + let(:user) { create(:user) } | |
| 398 | + let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, 'cd5c4bac', 'b1e6a9db') } | |
| 399 | + | |
| 400 | + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } | |
| 401 | + | |
| 402 | + it 'is sent to recipient' do | |
| 403 | + should deliver_to 'devs@company.name' | |
| 404 | + end | |
| 405 | + | |
| 406 | + it 'has the correct subject' do | |
| 407 | + should have_subject /New push to repository/ | |
| 408 | + end | |
| 409 | + | |
| 410 | + it 'includes commits list' do | |
| 411 | + should have_body_text /tree css fixes/ | |
| 412 | + end | |
| 413 | + | |
| 414 | + it 'includes diffs' do | |
| 415 | + should have_body_text /Checkout wiki pages for installation information/ | |
| 416 | + end | |
| 417 | + end | |
| 394 | 418 | end | ... | ... |