Commit 4f067ae931bef908312bbf7162abb3b6fdb85f8d
Exists in
master
and in
4 other branches
Merge branch 'feature/event_hooks' of /home/git/repositories/gitlab/gitlabhq
Showing
27 changed files
with
447 additions
and
268 deletions
Show diff stats
CHANGELOG
| ... | ... | @@ -7,6 +7,7 @@ v 6.4.0 |
| 7 | 7 | - Side-by-side diff view (Steven Thonus) |
| 8 | 8 | - Internal projects (Jason Hollingsworth) |
| 9 | 9 | - Allow removal of avatar (Drew Blessing) |
| 10 | + - Project web hooks now support issues and merge request events | |
| 10 | 11 | |
| 11 | 12 | v 6.3.0 |
| 12 | 13 | - API for adding gitlab-ci service | ... | ... |
app/assets/stylesheets/gitlab_bootstrap/forms.scss
app/helpers/application_helper.rb
| ... | ... | @@ -207,4 +207,12 @@ module ApplicationHelper |
| 207 | 207 | def broadcast_message |
| 208 | 208 | BroadcastMessage.current |
| 209 | 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 | 218 | end | ... | ... |
app/models/concerns/issuable.rb
app/models/project.rb
| ... | ... | @@ -298,8 +298,10 @@ class Project < ActiveRecord::Base |
| 298 | 298 | ProjectTransferService.new.transfer(self, new_namespace) |
| 299 | 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 | 305 | end |
| 304 | 306 | |
| 305 | 307 | def execute_services(data) | ... | ... |
app/models/project_hook.rb
| ... | ... | @@ -2,15 +2,24 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | class ProjectHook < WebHook |
| 15 | 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 | 25 | end | ... | ... |
app/models/service_hook.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | class ServiceHook < WebHook | ... | ... |
app/models/system_hook.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | class SystemHook < WebHook | ... | ... |
app/models/web_hook.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | class WebHook < ActiveRecord::Base | ... | ... |
app/observers/issue_observer.rb
| 1 | 1 | class IssueObserver < BaseObserver |
| 2 | 2 | def after_create(issue) |
| 3 | 3 | notification.new_issue(issue, current_user) |
| 4 | - | |
| 5 | 4 | issue.create_cross_references!(issue.project, current_user) |
| 5 | + execute_hooks(issue) | |
| 6 | 6 | end |
| 7 | 7 | |
| 8 | 8 | def after_close(issue, transition) |
| 9 | 9 | notification.close_issue(issue, current_user) |
| 10 | - | |
| 11 | 10 | create_note(issue) |
| 11 | + execute_hooks(issue) | |
| 12 | 12 | end |
| 13 | 13 | |
| 14 | 14 | def after_reopen(issue, transition) |
| ... | ... | @@ -29,4 +29,8 @@ class IssueObserver < BaseObserver |
| 29 | 29 | def create_note(issue) |
| 30 | 30 | Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit) |
| 31 | 31 | end |
| 32 | + | |
| 33 | + def execute_hooks(issue) | |
| 34 | + issue.project.execute_hooks(issue.to_hook_data, :issue_hooks) | |
| 35 | + end | |
| 32 | 36 | end | ... | ... |
app/observers/merge_request_observer.rb
| ... | ... | @@ -7,15 +7,15 @@ class MergeRequestObserver < ActivityObserver |
| 7 | 7 | end |
| 8 | 8 | |
| 9 | 9 | notification.new_merge_request(merge_request, current_user) |
| 10 | - | |
| 11 | 10 | merge_request.create_cross_references!(merge_request.project, current_user) |
| 11 | + execute_hooks(merge_request) | |
| 12 | 12 | end |
| 13 | 13 | |
| 14 | 14 | def after_close(merge_request, transition) |
| 15 | 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 | 16 | notification.close_mr(merge_request, current_user) |
| 17 | + create_note(merge_request) | |
| 18 | + execute_hooks(merge_request) | |
| 19 | 19 | end |
| 20 | 20 | |
| 21 | 21 | def after_merge(merge_request, transition) |
| ... | ... | @@ -31,11 +31,13 @@ class MergeRequestObserver < ActivityObserver |
| 31 | 31 | action: Event::MERGED, |
| 32 | 32 | author_id: merge_request.author_id_of_changes |
| 33 | 33 | ) |
| 34 | + | |
| 35 | + execute_hooks(merge_request) | |
| 34 | 36 | end |
| 35 | 37 | |
| 36 | 38 | def after_reopen(merge_request, transition) |
| 37 | 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 | 41 | end |
| 40 | 42 | |
| 41 | 43 | def after_update(merge_request) |
| ... | ... | @@ -53,4 +55,17 @@ class MergeRequestObserver < ActivityObserver |
| 53 | 55 | author_id: current_user.id |
| 54 | 56 | ) |
| 55 | 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 | 71 | end | ... | ... |
app/services/git_push_service.rb
app/views/help/web_hooks.html.haml
| 1 | 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 | 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 | 11 | %br |
| 7 | 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 | 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 | -<% 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 | 1 | %h3.page-title |
| 2 | - Post-receive hooks | |
| 2 | + Web hooks | |
| 3 | 3 | |
| 4 | 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 | 8 | %hr.clearfix |
| 9 | 9 | |
| ... | ... | @@ -13,23 +13,50 @@ |
| 13 | 13 | - @hook.errors.full_messages.each do |msg| |
| 14 | 14 | %p= msg |
| 15 | 15 | .control-group |
| 16 | - = f.label :url, "URL:" | |
| 16 | + = f.label :url, "URL" | |
| 17 | 17 | .controls |
| 18 | 18 | = f.text_field :url, class: "text_field input-xxlarge input-xpadding", placeholder: 'http://example.com/trigger-ci.json' |
| 19 | 19 | |
| 20 | 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 | 45 | %hr |
| 22 | 46 | |
| 23 | 47 | -if @hooks.any? |
| 24 | 48 | .ui-box |
| 25 | 49 | .title |
| 26 | - Hooks (#{@hooks.count}) | |
| 50 | + Web Hooks (#{@hooks.count}) | |
| 27 | 51 | %ul.well-list |
| 28 | 52 | - @hooks.each do |hook| |
| 29 | 53 | %li |
| 30 | - %span.badge.badge-info POST | |
| 31 | - → | |
| 32 | - %span.monospace= hook.url | |
| 33 | 54 | .pull-right |
| 34 | 55 | = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small grouped" |
| 35 | 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 @@ |
| 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 | ... | ... |
db/schema.rb
| ... | ... | @@ -11,7 +11,7 @@ |
| 11 | 11 | # |
| 12 | 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 | 16 | create_table "broadcast_messages", :force => true do |t| |
| 17 | 17 | t.text "message", :null => false |
| ... | ... | @@ -334,10 +334,13 @@ ActiveRecord::Schema.define(:version => 20131112220935) do |
| 334 | 334 | create_table "web_hooks", :force => true do |t| |
| 335 | 335 | t.string "url" |
| 336 | 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 | 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 | 344 | end |
| 342 | 345 | |
| 343 | 346 | add_index "web_hooks", ["project_id"], :name => "index_web_hooks_on_project_id" | ... | ... |
doc/api/projects.md
| ... | ... | @@ -402,6 +402,10 @@ Parameters: |
| 402 | 402 | { |
| 403 | 403 | "id": 1, |
| 404 | 404 | "url": "http://example.com/hook", |
| 405 | + "project_id": 3, | |
| 406 | + "push_events": "true", | |
| 407 | + "issues_events": "true", | |
| 408 | + "merge_requests_events": "true", | |
| 405 | 409 | "created_at": "2012-10-12T17:04:47Z" |
| 406 | 410 | } |
| 407 | 411 | ``` |
| ... | ... | @@ -419,6 +423,9 @@ Parameters: |
| 419 | 423 | |
| 420 | 424 | + `id` (required) - The ID or NAME of a project |
| 421 | 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 | 431 | ### Edit project hook |
| ... | ... | @@ -434,6 +441,9 @@ Parameters: |
| 434 | 441 | + `id` (required) - The ID or NAME of a project |
| 435 | 442 | + `hook_id` (required) - The ID of a project hook |
| 436 | 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 | 449 | ### Delete project hook | ... | ... |
lib/api/entities.rb
| ... | ... | @@ -24,6 +24,10 @@ module API |
| 24 | 24 | expose :id, :url, :created_at |
| 25 | 25 | end |
| 26 | 26 | |
| 27 | + class ProjectHook < Hook | |
| 28 | + expose :project_id, :push_events, :issues_events, :merge_requests_events | |
| 29 | + end | |
| 30 | + | |
| 27 | 31 | class ForkedFromProject < Grape::Entity |
| 28 | 32 | expose :id |
| 29 | 33 | expose :name, :name_with_namespace | ... | ... |
lib/api/project_hooks.rb
| ... | ... | @@ -22,7 +22,7 @@ module API |
| 22 | 22 | # GET /projects/:id/hooks |
| 23 | 23 | get ":id/hooks" do |
| 24 | 24 | @hooks = paginate user_project.hooks |
| 25 | - present @hooks, with: Entities::Hook | |
| 25 | + present @hooks, with: Entities::ProjectHook | |
| 26 | 26 | end |
| 27 | 27 | |
| 28 | 28 | # Get a project hook |
| ... | ... | @@ -34,7 +34,7 @@ module API |
| 34 | 34 | # GET /projects/:id/hooks/:hook_id |
| 35 | 35 | get ":id/hooks/:hook_id" do |
| 36 | 36 | @hook = user_project.hooks.find(params[:hook_id]) |
| 37 | - present @hook, with: Entities::Hook | |
| 37 | + present @hook, with: Entities::ProjectHook | |
| 38 | 38 | end |
| 39 | 39 | |
| 40 | 40 | |
| ... | ... | @@ -47,10 +47,11 @@ module API |
| 47 | 47 | # POST /projects/:id/hooks |
| 48 | 48 | post ":id/hooks" do |
| 49 | 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 | 53 | if @hook.save |
| 53 | - present @hook, with: Entities::Hook | |
| 54 | + present @hook, with: Entities::ProjectHook | |
| 54 | 55 | else |
| 55 | 56 | if @hook.errors[:url].present? |
| 56 | 57 | error!("Invalid url given", 422) |
| ... | ... | @@ -70,10 +71,10 @@ module API |
| 70 | 71 | put ":id/hooks/:hook_id" do |
| 71 | 72 | @hook = user_project.hooks.find(params[:hook_id]) |
| 72 | 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 | 76 | if @hook.update_attributes attrs |
| 76 | - present @hook, with: Entities::Hook | |
| 77 | + present @hook, with: Entities::ProjectHook | |
| 77 | 78 | else |
| 78 | 79 | if @hook.errors[:url].present? |
| 79 | 80 | error!("Invalid url given", 422) | ... | ... |
spec/models/service_hook_spec.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | require "spec_helper" | ... | ... |
spec/models/system_hook_spec.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | require "spec_helper" | ... | ... |
spec/models/web_hook_spec.rb
| ... | ... | @@ -2,13 +2,16 @@ |
| 2 | 2 | # |
| 3 | 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 | 17 | require 'spec_helper' | ... | ... |
spec/observers/merge_request_observer_spec.rb
| ... | ... | @@ -4,7 +4,7 @@ describe MergeRequestObserver do |
| 4 | 4 | let(:some_user) { create :user } |
| 5 | 5 | let(:assignee) { create :user } |
| 6 | 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 | 8 | let(:assigned_mr) { create(:merge_request, assignee: assignee, author: author, target_project: create(:project)) } |
| 9 | 9 | let(:unassigned_mr) { create(:merge_request, author: author, target_project: create(:project)) } |
| 10 | 10 | let(:closed_assigned_mr) { create(:closed_merge_request, assignee: assignee, author: author, target_project: create(:project)) } | ... | ... |
| ... | ... | @@ -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 | 10 | let(:user3) { create(:user) } |
| 11 | 11 | let(:admin) { create(:admin) } |
| 12 | 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 | 13 | let!(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } |
| 15 | 14 | let!(:users_project) { create(:users_project, user: user, project: project, project_access: UsersProject::MASTER) } |
| 16 | 15 | let!(:users_project2) { create(:users_project, user: user3, project: project, project_access: UsersProject::DEVELOPER) } |
| ... | ... | @@ -439,121 +438,6 @@ describe API::API do |
| 439 | 438 | end |
| 440 | 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 | 441 | describe "GET /projects/:id/snippets" do |
| 558 | 442 | it "should return an array of project snippets" do |
| 559 | 443 | get api("/projects/#{project.id}/snippets", user) | ... | ... |
spec/services/git_push_service_spec.rb
| ... | ... | @@ -74,38 +74,19 @@ describe GitPushService do |
| 74 | 74 | end |
| 75 | 75 | |
| 76 | 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 | 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 | 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 | 80 | service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') |
| 105 | 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 | 88 | it "when pushing tags" do |
| 108 | - @project_hook.should_not_receive(:async_execute) | |
| 89 | + project.should_not_receive(:execute_hooks) | |
| 109 | 90 | service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') |
| 110 | 91 | end |
| 111 | 92 | end | ... | ... |