Commit 4f067ae931bef908312bbf7162abb3b6fdb85f8d

Authored by Dmitriy Zaporozhets
2 parents d4f94a5b 21f4e5d3

Merge branch 'feature/event_hooks' of /home/git/repositories/gitlab/gitlabhq

@@ -7,6 +7,7 @@ v 6.4.0 @@ -7,6 +7,7 @@ v 6.4.0
7 - Side-by-side diff view (Steven Thonus) 7 - Side-by-side diff view (Steven Thonus)
8 - Internal projects (Jason Hollingsworth) 8 - Internal projects (Jason Hollingsworth)
9 - Allow removal of avatar (Drew Blessing) 9 - Allow removal of avatar (Drew Blessing)
  10 + - Project web hooks now support issues and merge request events
10 11
11 v 6.3.0 12 v 6.3.0
12 - API for adding gitlab-ci service 13 - API for adding gitlab-ci service
app/assets/stylesheets/gitlab_bootstrap/forms.scss
@@ -13,6 +13,13 @@ form { @@ -13,6 +13,13 @@ form {
13 margin-top: 1px !important; 13 margin-top: 1px !important;
14 } 14 }
15 } 15 }
  16 +
  17 + &.list-label {
  18 + float: none;
  19 + padding: 0 !important;
  20 + margin: 0;
  21 + text-align: left;
  22 + }
16 } 23 }
17 } 24 }
18 25
app/helpers/application_helper.rb
@@ -207,4 +207,12 @@ module ApplicationHelper @@ -207,4 +207,12 @@ module ApplicationHelper
207 def broadcast_message 207 def broadcast_message
208 BroadcastMessage.current 208 BroadcastMessage.current
209 end 209 end
  210 +
  211 + def highlight_js(&block)
  212 + string = capture(&block)
  213 +
  214 + content_tag :div, class: user_color_scheme_class do
  215 + Pygments::Lexer[:js].highlight(string).html_safe
  216 + end
  217 + end
210 end 218 end
app/models/concerns/issuable.rb
@@ -111,4 +111,11 @@ module Issuable @@ -111,4 +111,11 @@ module Issuable
111 end 111 end
112 users.concat(mentions.reduce([], :|)).uniq 112 users.concat(mentions.reduce([], :|)).uniq
113 end 113 end
  114 +
  115 + def to_hook_data
  116 + {
  117 + object_kind: self.class.name.underscore,
  118 + object_attributes: self.attributes
  119 + }
  120 + end
114 end 121 end
app/models/project.rb
@@ -298,8 +298,10 @@ class Project < ActiveRecord::Base @@ -298,8 +298,10 @@ class Project < ActiveRecord::Base
298 ProjectTransferService.new.transfer(self, new_namespace) 298 ProjectTransferService.new.transfer(self, new_namespace)
299 end 299 end
300 300
301 - def execute_hooks(data)  
302 - hooks.each { |hook| hook.async_execute(data) } 301 + def execute_hooks(data, hooks_scope = :push_hooks)
  302 + hooks.send(hooks_scope).each do |hook|
  303 + hook.async_execute(data)
  304 + end
303 end 305 end
304 306
305 def execute_services(data) 307 def execute_services(data)
app/models/project_hook.rb
@@ -2,15 +2,24 @@ @@ -2,15 +2,24 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 class ProjectHook < WebHook 17 class ProjectHook < WebHook
15 belongs_to :project 18 belongs_to :project
  19 +
  20 + attr_accessible :push_events, :issues_events, :merge_requests_events
  21 +
  22 + scope :push_hooks, -> { where(push_events: true) }
  23 + scope :issue_hooks, -> { where(issues_events: true) }
  24 + scope :merge_request_hooks, -> { where(merge_requests_events: true) }
16 end 25 end
app/models/service_hook.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 class ServiceHook < WebHook 17 class ServiceHook < WebHook
app/models/system_hook.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 class SystemHook < WebHook 17 class SystemHook < WebHook
app/models/web_hook.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 class WebHook < ActiveRecord::Base 17 class WebHook < ActiveRecord::Base
app/observers/issue_observer.rb
1 class IssueObserver < BaseObserver 1 class IssueObserver < BaseObserver
2 def after_create(issue) 2 def after_create(issue)
3 notification.new_issue(issue, current_user) 3 notification.new_issue(issue, current_user)
4 -  
5 issue.create_cross_references!(issue.project, current_user) 4 issue.create_cross_references!(issue.project, current_user)
  5 + execute_hooks(issue)
6 end 6 end
7 7
8 def after_close(issue, transition) 8 def after_close(issue, transition)
9 notification.close_issue(issue, current_user) 9 notification.close_issue(issue, current_user)
10 -  
11 create_note(issue) 10 create_note(issue)
  11 + execute_hooks(issue)
12 end 12 end
13 13
14 def after_reopen(issue, transition) 14 def after_reopen(issue, transition)
@@ -29,4 +29,8 @@ class IssueObserver &lt; BaseObserver @@ -29,4 +29,8 @@ class IssueObserver &lt; BaseObserver
29 def create_note(issue) 29 def create_note(issue)
30 Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) 30 Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit)
31 end 31 end
  32 +
  33 + def execute_hooks(issue)
  34 + issue.project.execute_hooks(issue.to_hook_data, :issue_hooks)
  35 + end
32 end 36 end
app/observers/merge_request_observer.rb
@@ -7,15 +7,15 @@ class MergeRequestObserver &lt; ActivityObserver @@ -7,15 +7,15 @@ class MergeRequestObserver &lt; ActivityObserver
7 end 7 end
8 8
9 notification.new_merge_request(merge_request, current_user) 9 notification.new_merge_request(merge_request, current_user)
10 -  
11 merge_request.create_cross_references!(merge_request.project, current_user) 10 merge_request.create_cross_references!(merge_request.project, current_user)
  11 + execute_hooks(merge_request)
12 end 12 end
13 13
14 def after_close(merge_request, transition) 14 def after_close(merge_request, transition)
15 create_event(merge_request, Event::CLOSED) 15 create_event(merge_request, Event::CLOSED)
16 - Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)  
17 -  
18 notification.close_mr(merge_request, current_user) 16 notification.close_mr(merge_request, current_user)
  17 + create_note(merge_request)
  18 + execute_hooks(merge_request)
19 end 19 end
20 20
21 def after_merge(merge_request, transition) 21 def after_merge(merge_request, transition)
@@ -31,11 +31,13 @@ class MergeRequestObserver &lt; ActivityObserver @@ -31,11 +31,13 @@ class MergeRequestObserver &lt; ActivityObserver
31 action: Event::MERGED, 31 action: Event::MERGED,
32 author_id: merge_request.author_id_of_changes 32 author_id: merge_request.author_id_of_changes
33 ) 33 )
  34 +
  35 + execute_hooks(merge_request)
34 end 36 end
35 37
36 def after_reopen(merge_request, transition) 38 def after_reopen(merge_request, transition)
37 create_event(merge_request, Event::REOPENED) 39 create_event(merge_request, Event::REOPENED)
38 - Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) 40 + create_note(merge_request)
39 end 41 end
40 42
41 def after_update(merge_request) 43 def after_update(merge_request)
@@ -53,4 +55,17 @@ class MergeRequestObserver &lt; ActivityObserver @@ -53,4 +55,17 @@ class MergeRequestObserver &lt; ActivityObserver
53 author_id: current_user.id 55 author_id: current_user.id
54 ) 56 )
55 end 57 end
  58 +
  59 + private
  60 +
  61 + # Create merge request note with service comment like 'Status changed to closed'
  62 + def create_note(merge_request)
  63 + Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
  64 + end
  65 +
  66 + def execute_hooks(merge_request)
  67 + if merge_request.project
  68 + merge_request.project.execute_hooks(merge_request.to_hook_data, :merge_request_hooks)
  69 + end
  70 + end
56 end 71 end
app/services/git_push_service.rb
@@ -32,7 +32,7 @@ class GitPushService @@ -32,7 +32,7 @@ class GitPushService
32 end 32 end
33 33
34 if push_to_branch?(ref) 34 if push_to_branch?(ref)
35 - project.execute_hooks(@push_data.dup) 35 + project.execute_hooks(@push_data.dup, :push_hooks)
36 project.execute_services(@push_data.dup) 36 project.execute_services(@push_data.dup)
37 end 37 end
38 38
app/views/help/web_hooks.html.haml
1 = render layout: 'help/layout' do 1 = render layout: 'help/layout' do
2 - %h3.page-title Web hooks 2 + %h3.page-title Project web hooks
  3 + %p.light
  4 + Project web hooks allow you to trigger url if new code is pushed or new issue is created
  5 + %hr
3 6
4 %p.slead 7 %p.slead
5 - Every GitLab project can trigger a web server whenever the repo is pushed to. 8 + You can configure web hook to listen for specific events like pushes, issues, merge requests.
  9 + %br
  10 + GitLab will send POST request with data to web hook url.
6 %br 11 %br
7 Web Hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. 12 Web Hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
  13 + %hr
  14 +
  15 + %h4 Push events
  16 + %p.light
  17 + Triggered when you push to the repository except pushing tags.
8 %br 18 %br
9 - GitLab will send POST request with commits information on every push.  
10 - %h5 Hooks request example:  
11 - = render "projects/hooks/data_ex" 19 + Request body:
  20 + = highlight_js do
  21 + :erb
  22 + {
  23 + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
  24 + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  25 + "ref": "refs/heads/master",
  26 + "user_id": 4,
  27 + "user_name": "John Smith",
  28 + "project_id": 15,
  29 + "repository": {
  30 + "name": "Diaspora",
  31 + "url": "git@localhost:diaspora.git",
  32 + "description": "",
  33 + "homepage": "http://localhost/diaspora",
  34 + },
  35 + "commits": [
  36 + {
  37 + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
  38 + "message": "Update Catalan translation to e38cb41.",
  39 + "timestamp": "2011-12-12T14:27:31+02:00",
  40 + "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
  41 + "author": {
  42 + "name": "Jordi Mallach",
  43 + "email": "jordi@softcatala.org",
  44 + }
  45 + },
  46 + // ...
  47 + {
  48 + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  49 + "message": "fixed readme",
  50 + "timestamp": "2012-01-03T23:36:29+02:00",
  51 + "url": "http://localhost/diaspora/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  52 + "author": {
  53 + "name": "GitLab dev user",
  54 + "email": "gitlabdev@dv6700.(none)",
  55 + },
  56 + },
  57 + ],
  58 + "total_commits_count": 4,
  59 + };
  60 +
12 61
  62 + %h4.prepend-top-20 Issues events
  63 + %p.light
  64 + Triggered when new issue created or existing issue was closed.
  65 + %br
  66 + Request body:
  67 + = highlight_js do
  68 + :erb
  69 + {
  70 + "object_kind":"issue",
  71 + "object_attributes":{
  72 + "id":301,
  73 + "title":"New API: create/update/delete file",
  74 + "assignee_id":51,
  75 + "author_id":51,
  76 + "project_id":14,
  77 + "created_at":"2013-12-03T17:15:43Z",
  78 + "updated_at":"2013-12-03T17:15:43Z",
  79 + "position":0,
  80 + "branch_name":null,
  81 + "description":"Create new API for manipulations with repository",
  82 + "milestone_id":null,
  83 + "state":"opened",
  84 + "iid":23
  85 + }
  86 + }
  87 + %h4.prepend-top-20 Merge request events
  88 + %p.light
  89 + Triggered when new merge request created or existing merge request was merged/closed.
  90 + %br
  91 + Request body:
  92 + = highlight_js do
  93 + :erb
  94 + {
  95 + "object_kind":"merge_request",
  96 + "object_attributes":{
  97 + "id":99,
  98 + "target_branch":"master",
  99 + "source_branch":"ms-viewport",
  100 + "source_project_id":14,
  101 + "author_id":51,
  102 + "assignee_id":6,
  103 + "title":"MS-Viewport",
  104 + "created_at":"2013-12-03T17:23:34Z",
  105 + "updated_at":"2013-12-03T17:23:34Z",
  106 + "st_commits":null,
  107 + "st_diffs":null,
  108 + "milestone_id":null,
  109 + "state":"opened",
  110 + "merge_status":"unchecked",
  111 + "target_project_id":14,
  112 + "iid":1,
  113 + "description":""
  114 + }
  115 + }
app/views/projects/hooks/_data_ex.html.erb
@@ -1,44 +0,0 @@ @@ -1,44 +0,0 @@
1 -<% data_ex_str = <<eos  
2 -{  
3 - "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",  
4 - "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",  
5 - "ref": "refs/heads/master",  
6 - "user_id": 4,  
7 - "user_name": "John Smith",  
8 - "project_id": 15,  
9 - "repository": {  
10 - "name": "Diaspora",  
11 - "url": "git@localhost:diaspora.git",  
12 - "description": "",  
13 - "homepage": "http://localhost/diaspora",  
14 - },  
15 - "commits": [  
16 - {  
17 - "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",  
18 - "message": "Update Catalan translation to e38cb41.",  
19 - "timestamp": "2011-12-12T14:27:31+02:00",  
20 - "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",  
21 - "author": {  
22 - "name": "Jordi Mallach",  
23 - "email": "jordi@softcatala.org",  
24 - }  
25 - },  
26 - // ...  
27 - {  
28 - "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",  
29 - "message": "fixed readme",  
30 - "timestamp": "2012-01-03T23:36:29+02:00",  
31 - "url": "http://localhost/diaspora/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",  
32 - "author": {  
33 - "name": "GitLab dev user",  
34 - "email": "gitlabdev@dv6700.(none)",  
35 - },  
36 - },  
37 - ],  
38 - "total_commits_count": 4,  
39 -};  
40 -eos  
41 -%>  
42 -<div class="<%= user_color_scheme_class%>">  
43 - <%= raw Pygments::Lexer[:js].highlight(data_ex_str) %>  
44 -</div>  
app/views/projects/hooks/index.html.haml
1 %h3.page-title 1 %h3.page-title
2 - Post-receive hooks 2 + Web hooks
3 3
4 %p.light 4 %p.light
5 - #{link_to "Post-receive hooks ", help_web_hooks_path, class: "vlink"} can be  
6 - used for binding events when someone pushes to the repository. 5 + #{link_to "Web hooks ", help_web_hooks_path, class: "vlink"} can be
  6 + used for binding events when something happends to the the project.
7 7
8 %hr.clearfix 8 %hr.clearfix
9 9
@@ -13,23 +13,50 @@ @@ -13,23 +13,50 @@
13 - @hook.errors.full_messages.each do |msg| 13 - @hook.errors.full_messages.each do |msg|
14 %p= msg 14 %p= msg
15 .control-group 15 .control-group
16 - = f.label :url, "URL:" 16 + = f.label :url, "URL"
17 .controls 17 .controls
18 = f.text_field :url, class: "text_field input-xxlarge input-xpadding", placeholder: 'http://example.com/trigger-ci.json' 18 = f.text_field :url, class: "text_field input-xxlarge input-xpadding", placeholder: 'http://example.com/trigger-ci.json'
19 &nbsp; 19 &nbsp;
20 = f.submit "Add Web Hook", class: "btn btn-create" 20 = f.submit "Add Web Hook", class: "btn btn-create"
  21 + .control-group
  22 + = f.label :url, "Trigger"
  23 + .controls
  24 + %div
  25 + = f.check_box :push_events, class: 'pull-left'
  26 + .prepend-left-20
  27 + = f.label :push_events, class: 'list-label' do
  28 + %strong Push events
  29 + %p.light
  30 + This url will be triggered in case of push to repository
  31 + %div
  32 + = f.check_box :issues_events, class: 'pull-left'
  33 + .prepend-left-20
  34 + = f.label :issues_events, class: 'list-label' do
  35 + %strong Issues events
  36 + %p.light
  37 + This url will be triggered for created issues
  38 + %div
  39 + = f.check_box :merge_requests_events, class: 'pull-left'
  40 + .prepend-left-20
  41 + = f.label :merge_requests_events, class: 'list-label' do
  42 + %strong Merge Request events
  43 + %p.light
  44 + This url will be triggered for created merge requests
21 %hr 45 %hr
22 46
23 -if @hooks.any? 47 -if @hooks.any?
24 .ui-box 48 .ui-box
25 .title 49 .title
26 - Hooks (#{@hooks.count}) 50 + Web Hooks (#{@hooks.count})
27 %ul.well-list 51 %ul.well-list
28 - @hooks.each do |hook| 52 - @hooks.each do |hook|
29 %li 53 %li
30 - %span.badge.badge-info POST  
31 - &rarr;  
32 - %span.monospace= hook.url  
33 .pull-right 54 .pull-right
34 = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small grouped" 55 = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small grouped"
35 = link_to 'Remove', project_hook_path(@project, hook), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove btn-small grouped" 56 = link_to 'Remove', project_hook_path(@project, hook), confirm: 'Are you sure?', method: :delete, class: "btn btn-remove btn-small grouped"
  57 + .clearfix
  58 + %span.monospace= hook.url
  59 + %p
  60 + - %w(push_events issues_events merge_requests_events).each do |trigger|
  61 + - if hook.send(trigger)
  62 + %span.label.label-gray= trigger.titleize
db/migrate/20131202192556_add_event_fields_for_web_hook.rb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +class AddEventFieldsForWebHook < ActiveRecord::Migration
  2 + def change
  3 + add_column :web_hooks, :push_events, :boolean, default: true, null: false
  4 + add_column :web_hooks, :issues_events, :boolean, default: false, null: false
  5 + add_column :web_hooks, :merge_requests_events, :boolean, default: false, null: false
  6 + end
  7 +end
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended to check this file into your version control system. 12 # It's strongly recommended to check this file into your version control system.
13 13
14 -ActiveRecord::Schema.define(:version => 20131112220935) do 14 +ActiveRecord::Schema.define(:version => 20131202192556) do
15 15
16 create_table "broadcast_messages", :force => true do |t| 16 create_table "broadcast_messages", :force => true do |t|
17 t.text "message", :null => false 17 t.text "message", :null => false
@@ -334,10 +334,13 @@ ActiveRecord::Schema.define(:version =&gt; 20131112220935) do @@ -334,10 +334,13 @@ ActiveRecord::Schema.define(:version =&gt; 20131112220935) do
334 create_table "web_hooks", :force => true do |t| 334 create_table "web_hooks", :force => true do |t|
335 t.string "url" 335 t.string "url"
336 t.integer "project_id" 336 t.integer "project_id"
337 - t.datetime "created_at", :null => false  
338 - t.datetime "updated_at", :null => false  
339 - t.string "type", :default => "ProjectHook" 337 + t.datetime "created_at", :null => false
  338 + t.datetime "updated_at", :null => false
  339 + t.string "type", :default => "ProjectHook"
340 t.integer "service_id" 340 t.integer "service_id"
  341 + t.boolean "push_events", :default => true, :null => false
  342 + t.boolean "issues_events", :default => false, :null => false
  343 + t.boolean "merge_requests_events", :default => false, :null => false
341 end 344 end
342 345
343 add_index "web_hooks", ["project_id"], :name => "index_web_hooks_on_project_id" 346 add_index "web_hooks", ["project_id"], :name => "index_web_hooks_on_project_id"
doc/api/projects.md
@@ -402,6 +402,10 @@ Parameters: @@ -402,6 +402,10 @@ Parameters:
402 { 402 {
403 "id": 1, 403 "id": 1,
404 "url": "http://example.com/hook", 404 "url": "http://example.com/hook",
  405 + "project_id": 3,
  406 + "push_events": "true",
  407 + "issues_events": "true",
  408 + "merge_requests_events": "true",
405 "created_at": "2012-10-12T17:04:47Z" 409 "created_at": "2012-10-12T17:04:47Z"
406 } 410 }
407 ``` 411 ```
@@ -419,6 +423,9 @@ Parameters: @@ -419,6 +423,9 @@ Parameters:
419 423
420 + `id` (required) - The ID or NAME of a project 424 + `id` (required) - The ID or NAME of a project
421 + `url` (required) - The hook URL 425 + `url` (required) - The hook URL
  426 ++ `push_events` - Trigger hook on push events
  427 ++ `issues_events` - Trigger hook on issues events
  428 ++ `merge_requests_events` - Trigger hook on merge_requests events
422 429
423 430
424 ### Edit project hook 431 ### Edit project hook
@@ -434,6 +441,9 @@ Parameters: @@ -434,6 +441,9 @@ Parameters:
434 + `id` (required) - The ID or NAME of a project 441 + `id` (required) - The ID or NAME of a project
435 + `hook_id` (required) - The ID of a project hook 442 + `hook_id` (required) - The ID of a project hook
436 + `url` (required) - The hook URL 443 + `url` (required) - The hook URL
  444 ++ `push_events` - Trigger hook on push events
  445 ++ `issues_events` - Trigger hook on issues events
  446 ++ `merge_requests_events` - Trigger hook on merge_requests events
437 447
438 448
439 ### Delete project hook 449 ### Delete project hook
lib/api/entities.rb
@@ -24,6 +24,10 @@ module API @@ -24,6 +24,10 @@ module API
24 expose :id, :url, :created_at 24 expose :id, :url, :created_at
25 end 25 end
26 26
  27 + class ProjectHook < Hook
  28 + expose :project_id, :push_events, :issues_events, :merge_requests_events
  29 + end
  30 +
27 class ForkedFromProject < Grape::Entity 31 class ForkedFromProject < Grape::Entity
28 expose :id 32 expose :id
29 expose :name, :name_with_namespace 33 expose :name, :name_with_namespace
lib/api/project_hooks.rb
@@ -22,7 +22,7 @@ module API @@ -22,7 +22,7 @@ module API
22 # GET /projects/:id/hooks 22 # GET /projects/:id/hooks
23 get ":id/hooks" do 23 get ":id/hooks" do
24 @hooks = paginate user_project.hooks 24 @hooks = paginate user_project.hooks
25 - present @hooks, with: Entities::Hook 25 + present @hooks, with: Entities::ProjectHook
26 end 26 end
27 27
28 # Get a project hook 28 # Get a project hook
@@ -34,7 +34,7 @@ module API @@ -34,7 +34,7 @@ module API
34 # GET /projects/:id/hooks/:hook_id 34 # GET /projects/:id/hooks/:hook_id
35 get ":id/hooks/:hook_id" do 35 get ":id/hooks/:hook_id" do
36 @hook = user_project.hooks.find(params[:hook_id]) 36 @hook = user_project.hooks.find(params[:hook_id])
37 - present @hook, with: Entities::Hook 37 + present @hook, with: Entities::ProjectHook
38 end 38 end
39 39
40 40
@@ -47,10 +47,11 @@ module API @@ -47,10 +47,11 @@ module API
47 # POST /projects/:id/hooks 47 # POST /projects/:id/hooks
48 post ":id/hooks" do 48 post ":id/hooks" do
49 required_attributes! [:url] 49 required_attributes! [:url]
  50 + attrs = attributes_for_keys [:url, :push_events, :issues_events, :merge_requests_events]
  51 + @hook = user_project.hooks.new(attrs)
50 52
51 - @hook = user_project.hooks.new({"url" => params[:url]})  
52 if @hook.save 53 if @hook.save
53 - present @hook, with: Entities::Hook 54 + present @hook, with: Entities::ProjectHook
54 else 55 else
55 if @hook.errors[:url].present? 56 if @hook.errors[:url].present?
56 error!("Invalid url given", 422) 57 error!("Invalid url given", 422)
@@ -70,10 +71,10 @@ module API @@ -70,10 +71,10 @@ module API
70 put ":id/hooks/:hook_id" do 71 put ":id/hooks/:hook_id" do
71 @hook = user_project.hooks.find(params[:hook_id]) 72 @hook = user_project.hooks.find(params[:hook_id])
72 required_attributes! [:url] 73 required_attributes! [:url]
  74 + attrs = attributes_for_keys [:url, :push_events, :issues_events, :merge_requests_events]
73 75
74 - attrs = attributes_for_keys [:url]  
75 if @hook.update_attributes attrs 76 if @hook.update_attributes attrs
76 - present @hook, with: Entities::Hook 77 + present @hook, with: Entities::ProjectHook
77 else 78 else
78 if @hook.errors[:url].present? 79 if @hook.errors[:url].present?
79 error!("Invalid url given", 422) 80 error!("Invalid url given", 422)
spec/models/service_hook_spec.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 require "spec_helper" 17 require "spec_helper"
spec/models/system_hook_spec.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 require "spec_helper" 17 require "spec_helper"
spec/models/web_hook_spec.rb
@@ -2,13 +2,16 @@ @@ -2,13 +2,16 @@
2 # 2 #
3 # Table name: web_hooks 3 # Table name: web_hooks
4 # 4 #
5 -# id :integer not null, primary key  
6 -# url :string(255)  
7 -# project_id :integer  
8 -# created_at :datetime not null  
9 -# updated_at :datetime not null  
10 -# type :string(255) default("ProjectHook")  
11 -# service_id :integer 5 +# id :integer not null, primary key
  6 +# url :string(255)
  7 +# project_id :integer
  8 +# created_at :datetime not null
  9 +# updated_at :datetime not null
  10 +# type :string(255) default("ProjectHook")
  11 +# service_id :integer
  12 +# push_events :boolean default(TRUE), not null
  13 +# issues_events :boolean default(FALSE), not null
  14 +# merge_requests_events :boolean default(FALSE), not null
12 # 15 #
13 16
14 require 'spec_helper' 17 require 'spec_helper'
spec/observers/merge_request_observer_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequestObserver do @@ -4,7 +4,7 @@ describe MergeRequestObserver do
4 let(:some_user) { create :user } 4 let(:some_user) { create :user }
5 let(:assignee) { create :user } 5 let(:assignee) { create :user }
6 let(:author) { create :user } 6 let(:author) { create :user }
7 - let(:mr_mock) { double(:merge_request, id: 42, assignee: assignee, author: author) } 7 + let(:mr_mock) { double(:merge_request, id: 42, assignee: assignee, author: author).as_null_object }
8 let(:assigned_mr) { create(:merge_request, assignee: assignee, author: author, target_project: create(:project)) } 8 let(:assigned_mr) { create(:merge_request, assignee: assignee, author: author, target_project: create(:project)) }
9 let(:unassigned_mr) { create(:merge_request, author: author, target_project: create(:project)) } 9 let(:unassigned_mr) { create(:merge_request, author: author, target_project: create(:project)) }
10 let(:closed_assigned_mr) { create(:closed_merge_request, assignee: assignee, author: author, target_project: create(:project)) } 10 let(:closed_assigned_mr) { create(:closed_merge_request, assignee: assignee, author: author, target_project: create(:project)) }
spec/requests/api/project_hooks_spec.rb 0 → 100644
@@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
  1 +require 'spec_helper'
  2 +
  3 +describe API::API, 'ProjectHooks' do
  4 + include ApiHelpers
  5 + before(:each) { enable_observers }
  6 + after(:each) { disable_observers }
  7 +
  8 + let(:user) { create(:user) }
  9 + let(:user3) { create(:user) }
  10 + let!(:project) { create(:project_with_code, creator_id: user.id, namespace: user.namespace) }
  11 + let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }
  12 +
  13 + before do
  14 + project.team << [user, :master]
  15 + project.team << [user3, :developer]
  16 + end
  17 +
  18 + describe "GET /projects/:id/hooks" do
  19 + context "authorized user" do
  20 + it "should return project hooks" do
  21 + get api("/projects/#{project.id}/hooks", user)
  22 + response.status.should == 200
  23 +
  24 + json_response.should be_an Array
  25 + json_response.count.should == 1
  26 + json_response.first['url'].should == "http://example.com"
  27 + end
  28 + end
  29 +
  30 + context "unauthorized user" do
  31 + it "should not access project hooks" do
  32 + get api("/projects/#{project.id}/hooks", user3)
  33 + response.status.should == 403
  34 + end
  35 + end
  36 + end
  37 +
  38 + describe "GET /projects/:id/hooks/:hook_id" do
  39 + context "authorized user" do
  40 + it "should return a project hook" do
  41 + get api("/projects/#{project.id}/hooks/#{hook.id}", user)
  42 + response.status.should == 200
  43 + json_response['url'].should == hook.url
  44 + end
  45 +
  46 + it "should return a 404 error if hook id is not available" do
  47 + get api("/projects/#{project.id}/hooks/1234", user)
  48 + response.status.should == 404
  49 + end
  50 + end
  51 +
  52 + context "unauthorized user" do
  53 + it "should not access an existing hook" do
  54 + get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
  55 + response.status.should == 403
  56 + end
  57 + end
  58 +
  59 + it "should return a 404 error if hook id is not available" do
  60 + get api("/projects/#{project.id}/hooks/1234", user)
  61 + response.status.should == 404
  62 + end
  63 + end
  64 +
  65 + describe "POST /projects/:id/hooks" do
  66 + it "should add hook to project" do
  67 + expect {
  68 + post api("/projects/#{project.id}/hooks", user),
  69 + url: "http://example.com", issues_events: true
  70 + }.to change {project.hooks.count}.by(1)
  71 + response.status.should == 201
  72 + end
  73 +
  74 + it "should return a 400 error if url not given" do
  75 + post api("/projects/#{project.id}/hooks", user)
  76 + response.status.should == 400
  77 + end
  78 +
  79 + it "should return a 422 error if url not valid" do
  80 + post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
  81 + response.status.should == 422
  82 + end
  83 + end
  84 +
  85 + describe "PUT /projects/:id/hooks/:hook_id" do
  86 + it "should update an existing project hook" do
  87 + put api("/projects/#{project.id}/hooks/#{hook.id}", user),
  88 + url: 'http://example.org', push_events: false
  89 + response.status.should == 200
  90 + json_response['url'].should == 'http://example.org'
  91 + end
  92 +
  93 + it "should return 404 error if hook id not found" do
  94 + put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
  95 + response.status.should == 404
  96 + end
  97 +
  98 + it "should return 400 error if url is not given" do
  99 + put api("/projects/#{project.id}/hooks/#{hook.id}", user)
  100 + response.status.should == 400
  101 + end
  102 +
  103 + it "should return a 422 error if url is not valid" do
  104 + put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
  105 + response.status.should == 422
  106 + end
  107 + end
  108 +
  109 + describe "DELETE /projects/:id/hooks/:hook_id" do
  110 + it "should delete hook from project" do
  111 + expect {
  112 + delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
  113 + }.to change {project.hooks.count}.by(-1)
  114 + response.status.should == 200
  115 + end
  116 +
  117 + it "should return success when deleting hook" do
  118 + delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
  119 + response.status.should == 200
  120 + end
  121 +
  122 + it "should return success when deleting non existent hook" do
  123 + delete api("/projects/#{project.id}/hooks/42", user)
  124 + response.status.should == 200
  125 + end
  126 +
  127 + it "should return a 405 error if hook id not given" do
  128 + delete api("/projects/#{project.id}/hooks", user)
  129 + response.status.should == 405
  130 + end
  131 + end
  132 +end
spec/requests/api/projects_spec.rb
@@ -10,7 +10,6 @@ describe API::API do @@ -10,7 +10,6 @@ describe API::API do
10 let(:user3) { create(:user) } 10 let(:user3) { create(:user) }
11 let(:admin) { create(:admin) } 11 let(:admin) { create(:admin) }
12 let!(:project) { create(:project_with_code, creator_id: user.id, namespace: user.namespace) } 12 let!(:project) { create(:project_with_code, creator_id: user.id, namespace: user.namespace) }
13 - let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }  
14 let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } 13 let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') }
15 let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } 14 let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) }
16 let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } 15 let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) }
@@ -439,121 +438,6 @@ describe API::API do @@ -439,121 +438,6 @@ describe API::API do
439 end 438 end
440 end 439 end
441 440
442 - describe "GET /projects/:id/hooks" do  
443 - context "authorized user" do  
444 - it "should return project hooks" do  
445 - get api("/projects/#{project.id}/hooks", user)  
446 - response.status.should == 200  
447 -  
448 - json_response.should be_an Array  
449 - json_response.count.should == 1  
450 - json_response.first['url'].should == "http://example.com"  
451 - end  
452 - end  
453 -  
454 - context "unauthorized user" do  
455 - it "should not access project hooks" do  
456 - get api("/projects/#{project.id}/hooks", user3)  
457 - response.status.should == 403  
458 - end  
459 - end  
460 - end  
461 -  
462 - describe "GET /projects/:id/hooks/:hook_id" do  
463 - context "authorized user" do  
464 - it "should return a project hook" do  
465 - get api("/projects/#{project.id}/hooks/#{hook.id}", user)  
466 - response.status.should == 200  
467 - json_response['url'].should == hook.url  
468 - end  
469 -  
470 - it "should return a 404 error if hook id is not available" do  
471 - get api("/projects/#{project.id}/hooks/1234", user)  
472 - response.status.should == 404  
473 - end  
474 - end  
475 -  
476 - context "unauthorized user" do  
477 - it "should not access an existing hook" do  
478 - get api("/projects/#{project.id}/hooks/#{hook.id}", user3)  
479 - response.status.should == 403  
480 - end  
481 - end  
482 -  
483 - it "should return a 404 error if hook id is not available" do  
484 - get api("/projects/#{project.id}/hooks/1234", user)  
485 - response.status.should == 404  
486 - end  
487 - end  
488 -  
489 - describe "POST /projects/:id/hooks" do  
490 - it "should add hook to project" do  
491 - expect {  
492 - post api("/projects/#{project.id}/hooks", user),  
493 - url: "http://example.com"  
494 - }.to change {project.hooks.count}.by(1)  
495 - response.status.should == 201  
496 - end  
497 -  
498 - it "should return a 400 error if url not given" do  
499 - post api("/projects/#{project.id}/hooks", user)  
500 - response.status.should == 400  
501 - end  
502 -  
503 - it "should return a 422 error if url not valid" do  
504 - post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"  
505 - response.status.should == 422  
506 - end  
507 - end  
508 -  
509 - describe "PUT /projects/:id/hooks/:hook_id" do  
510 - it "should update an existing project hook" do  
511 - put api("/projects/#{project.id}/hooks/#{hook.id}", user),  
512 - url: 'http://example.org'  
513 - response.status.should == 200  
514 - json_response['url'].should == 'http://example.org'  
515 - end  
516 -  
517 - it "should return 404 error if hook id not found" do  
518 - put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'  
519 - response.status.should == 404  
520 - end  
521 -  
522 - it "should return 400 error if url is not given" do  
523 - put api("/projects/#{project.id}/hooks/#{hook.id}", user)  
524 - response.status.should == 400  
525 - end  
526 -  
527 - it "should return a 422 error if url is not valid" do  
528 - put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'  
529 - response.status.should == 422  
530 - end  
531 - end  
532 -  
533 - describe "DELETE /projects/:id/hooks/:hook_id" do  
534 - it "should delete hook from project" do  
535 - expect {  
536 - delete api("/projects/#{project.id}/hooks/#{hook.id}", user)  
537 - }.to change {project.hooks.count}.by(-1)  
538 - response.status.should == 200  
539 - end  
540 -  
541 - it "should return success when deleting hook" do  
542 - delete api("/projects/#{project.id}/hooks/#{hook.id}", user)  
543 - response.status.should == 200  
544 - end  
545 -  
546 - it "should return success when deleting non existent hook" do  
547 - delete api("/projects/#{project.id}/hooks/42", user)  
548 - response.status.should == 200  
549 - end  
550 -  
551 - it "should return a 405 error if hook id not given" do  
552 - delete api("/projects/#{project.id}/hooks", user)  
553 - response.status.should == 405  
554 - end  
555 - end  
556 -  
557 describe "GET /projects/:id/snippets" do 441 describe "GET /projects/:id/snippets" do
558 it "should return an array of project snippets" do 442 it "should return an array of project snippets" do
559 get api("/projects/#{project.id}/snippets", user) 443 get api("/projects/#{project.id}/snippets", user)
spec/services/git_push_service_spec.rb
@@ -74,38 +74,19 @@ describe GitPushService do @@ -74,38 +74,19 @@ describe GitPushService do
74 end 74 end
75 75
76 describe "Web Hooks" do 76 describe "Web Hooks" do
77 - context "with web hooks" do  
78 - before do  
79 - @project_hook = create(:project_hook)  
80 - @project_hook_2 = create(:project_hook)  
81 - project.hooks << [@project_hook, @project_hook_2]  
82 -  
83 - stub_request(:post, @project_hook.url)  
84 - stub_request(:post, @project_hook_2.url)  
85 - end  
86 -  
87 - it "executes multiple web hook" do  
88 - @project_hook.should_receive(:async_execute).once  
89 - @project_hook_2.should_receive(:async_execute).once  
90 -  
91 - service.execute(project, user, @oldrev, @newrev, @ref)  
92 - end  
93 - end  
94 -  
95 context "execute web hooks" do 77 context "execute web hooks" do
96 - before do  
97 - @project_hook = create(:project_hook)  
98 - project.hooks << [@project_hook]  
99 - stub_request(:post, @project_hook.url)  
100 - end  
101 -  
102 it "when pushing a branch for the first time" do 78 it "when pushing a branch for the first time" do
103 - @project_hook.should_receive(:async_execute) 79 + project.should_receive(:execute_hooks)
104 service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') 80 service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master')
105 end 81 end
106 82
  83 + it "when pushing new commits to existing branch" do
  84 + project.should_receive(:execute_hooks)
  85 + service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master')
  86 + end
  87 +
107 it "when pushing tags" do 88 it "when pushing tags" do
108 - @project_hook.should_not_receive(:async_execute) 89 + project.should_not_receive(:execute_hooks)
109 service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') 90 service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0')
110 end 91 end
111 end 92 end