Commit 66fb3909a564d2e7905f5b720fd55ea61286605e

Authored by Valery Sizov
2 parents fbf412ea 5ca83604

Merge branch 'web_hooks' of https://github.com/ariejan/gitlabhq into ariejan-web_hooks

Conflicts:
	app/models/project.rb
	config/routes.rb
	db/schema.rb
Gemfile
... ... @@ -23,6 +23,8 @@ gem "rdiscount"
23 23 gem "acts-as-taggable-on", "~> 2.1.0"
24 24 gem "drapper"
25 25 gem "rchardet19", "~> 1.3.5"
  26 +gem "resque"
  27 +gem "httparty"
26 28  
27 29 group :assets do
28 30 gem "sass-rails", "~> 3.1.0"
... ... @@ -47,6 +49,7 @@ group :development, :test do
47 49 gem "awesome_print"
48 50 gem "database_cleaner"
49 51 gem "launchy"
  52 + gem "webmock"
50 53 end
51 54  
52 55 group :test do
... ...
Gemfile.lock
... ... @@ -87,6 +87,7 @@ GEM
87 87 execjs
88 88 coffee-script-source (1.1.3)
89 89 columnize (0.3.4)
  90 + crack (0.3.1)
90 91 daemons (1.1.4)
91 92 database_cleaner (0.7.0)
92 93 devise (1.5.0)
... ... @@ -111,6 +112,9 @@ GEM
111 112 railties (~> 3.0)
112 113 hashery (1.4.0)
113 114 hike (1.2.1)
  115 + httparty (0.8.1)
  116 + multi_json
  117 + multi_xml
114 118 i18n (0.6.0)
115 119 jquery-rails (1.0.17)
116 120 railties (~> 3.0)
... ... @@ -132,6 +136,7 @@ GEM
132 136 treetop (~> 1.4.8)
133 137 mime-types (1.17.2)
134 138 multi_json (1.0.3)
  139 + multi_xml (0.4.1)
135 140 nokogiri (1.5.0)
136 141 orm_adapter (0.0.5)
137 142 polyglot (0.3.3)
... ... @@ -143,6 +148,8 @@ GEM
143 148 rack (>= 0.4)
144 149 rack-mount (0.8.3)
145 150 rack (>= 1.0.0)
  151 + rack-protection (1.1.4)
  152 + rack
146 153 rack-ssl (1.3.2)
147 154 rack
148 155 rack-test (0.6.1)
... ... @@ -169,6 +176,14 @@ GEM
169 176 rdiscount (1.6.8)
170 177 rdoc (3.11)
171 178 json (~> 1.4)
  179 + redis (2.2.2)
  180 + redis-namespace (1.0.3)
  181 + redis (< 3.0.0)
  182 + resque (1.19.0)
  183 + multi_json (~> 1.0)
  184 + redis-namespace (~> 1.0.2)
  185 + sinatra (>= 0.9.2)
  186 + vegas (~> 0.1.2)
172 187 rspec (2.7.0)
173 188 rspec-core (~> 2.7.0)
174 189 rspec-expectations (~> 2.7.0)
... ... @@ -220,6 +235,10 @@ GEM
220 235 multi_json (~> 1.0.3)
221 236 simplecov-html (~> 0.5.3)
222 237 simplecov-html (0.5.3)
  238 + sinatra (1.3.1)
  239 + rack (~> 1.3, >= 1.3.4)
  240 + rack-protection (~> 1.1, >= 1.1.2)
  241 + tilt (~> 1.3, >= 1.3.3)
223 242 six (0.2.0)
224 243 sprockets (2.0.3)
225 244 hike (~> 1.2)
... ... @@ -244,8 +263,13 @@ GEM
244 263 uglifier (1.1.0)
245 264 execjs (>= 0.3.0)
246 265 multi_json (>= 1.0.2)
  266 + vegas (0.1.8)
  267 + rack (>= 1.0.0)
247 268 warden (1.1.0)
248 269 rack (>= 1.0)
  270 + webmock (1.7.8)
  271 + addressable (~> 2.2, > 2.2.5)
  272 + crack (>= 0.1.7)
249 273 xpath (0.1.4)
250 274 nokogiri (~> 1.3)
251 275  
... ... @@ -270,6 +294,7 @@ DEPENDENCIES
270 294 gitolite!
271 295 grit!
272 296 haml-rails
  297 + httparty
273 298 jquery-rails
274 299 kaminari
275 300 launchy
... ... @@ -279,6 +304,7 @@ DEPENDENCIES
279 304 rails-footnotes (~> 3.7.5)
280 305 rchardet19 (~> 1.3.5)
281 306 rdiscount
  307 + resque
282 308 rspec-rails
283 309 ruby-debug19
284 310 sass-rails (~> 3.1.0)
... ... @@ -292,3 +318,4 @@ DEPENDENCIES
292 318 thin
293 319 turn
294 320 uglifier
  321 + webmock
... ...
app/models/project.rb
... ... @@ -14,6 +14,7 @@ class Project &lt; ActiveRecord::Base
14 14 has_many :users, :through => :users_projects
15 15 has_many :notes, :dependent => :destroy
16 16 has_many :snippets, :dependent => :destroy
  17 + has_many :web_hooks, :dependent => :destroy
17 18  
18 19 acts_as_taggable
19 20  
... ... @@ -82,12 +83,58 @@ class Project &lt; ActiveRecord::Base
82 83 :heads,
83 84 :commits_since,
84 85 :fresh_commits,
  86 + :commits_between,
85 87 :to => :repository, :prefix => nil
86 88  
87 89 def to_param
88 90 code
89 91 end
90 92  
  93 + def web_url
  94 + [GIT_HOST['host'], code].join("/")
  95 + end
  96 +
  97 + def execute_web_hooks(oldrev, newrev, ref)
  98 + ref_parts = ref.split('/')
  99 +
  100 + # Return if this is not a push to a branch (e.g. new commits)
  101 + return if ref_parts[1] !~ /heads/ || oldrev == "00000000000000000000000000000000"
  102 +
  103 + data = web_hook_data(oldrev, newrev, ref)
  104 + web_hooks.each { |web_hook| web_hook.execute(data) }
  105 + end
  106 +
  107 + def web_hook_data(oldrev, newrev, ref)
  108 + data = {
  109 + before: oldrev,
  110 + after: newrev,
  111 + ref: ref,
  112 + repository: {
  113 + name: name,
  114 + url: web_url,
  115 + description: description,
  116 + homepage: web_url,
  117 + private: private?
  118 + },
  119 + commits: []
  120 + }
  121 +
  122 + commits_between(oldrev, newrev).each do |commit|
  123 + data[:commits] << {
  124 + id: commit.id,
  125 + message: commit.safe_message,
  126 + timestamp: commit.date.xmlschema,
  127 + url: "http://#{GIT_HOST['host']}/#{code}/commits/#{commit.id}",
  128 + author: {
  129 + name: commit.author_name,
  130 + email: commit.author_email
  131 + }
  132 + }
  133 + end
  134 +
  135 + data
  136 + end
  137 +
91 138 def team_member_by_name_or_email(email = nil, name = nil)
92 139 user = users.where("email like ? or name like ?", email, name).first
93 140 users_projects.find_by_user_id(user.id) if user
... ...
app/models/repository.rb
... ... @@ -31,6 +31,22 @@ class Repository
31 31 project.id
32 32 end
33 33  
  34 + def write_hooks
  35 + %w(post-receive).each do |hook|
  36 + write_hook(hook, File.read(File.join(Rails.root, 'lib', "#{hook}-hook")))
  37 + end
  38 + end
  39 +
  40 + def write_hook(name, content)
  41 + hook_file = File.join(project.path_to_repo, 'hooks', name)
  42 +
  43 + File.open(hook_file, 'w') do |f|
  44 + f.write(content)
  45 + end
  46 +
  47 + File.chmod(0775, hook_file)
  48 + end
  49 +
34 50 def repo
35 51 @repo ||= Grit::Repo.new(project.path_to_repo)
36 52 end
... ... @@ -47,6 +63,8 @@ class Repository
47 63 Gitlabhq::GitHost.system.new.configure do |c|
48 64 c.update_project(path, project)
49 65 end
  66 +
  67 + write_hooks
50 68 end
51 69  
52 70 def destroy_repository
... ... @@ -115,4 +133,8 @@ class Repository
115 133 repo.commits(ref)
116 134 end.map{ |c| Commit.new(c) }
117 135 end
  136 +
  137 + def commits_between(from, to)
  138 + repo.commits_between(from, to).map { |c| Commit.new(c) }
  139 + end
118 140 end
... ...
app/models/web_hook.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +class WebHook < ActiveRecord::Base
  2 + include HTTParty
  3 +
  4 + # HTTParty timeout
  5 + default_timeout 10
  6 +
  7 + belongs_to :project
  8 +
  9 + validates :url,
  10 + presence: true,
  11 + format: {
  12 + with: URI::regexp(%w(http https)),
  13 + message: "should be a valid url" }
  14 +
  15 + def execute(data)
  16 + WebHook.post(url, body: data.to_json)
  17 + rescue
  18 + # There was a problem calling this web hook, let's forget about it.
  19 + end
  20 +end
... ...
app/workers/post_receive.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +class PostReceive
  2 + def self.perform(reponame, oldrev, newrev, ref)
  3 + project = Project.find_by_path(reponame)
  4 + return false if project.nil?
  5 +
  6 + project.execute_web_hooks(oldrev, newrev, ref)
  7 + end
  8 +end
... ...
config/routes.rb
1 1 Gitlab::Application.routes.draw do
2 2  
  3 + # Optionally, enable Resque here
  4 + # require 'resque/server'
  5 + # mount Resque::Server.new, at: '/info/resque'
3 6  
4 7 get 'tags'=> 'tags#index'
5 8 get 'tags/:tag' => 'projects#index'
... ...
db/migrate/20111214091851_create_web_hooks.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +class CreateWebHooks < ActiveRecord::Migration
  2 + def change
  3 + create_table :web_hooks do |t|
  4 + t.string :url
  5 + t.integer :project_id
  6 + t.timestamps
  7 + end
  8 + end
  9 +end
... ...
db/schema.rb
... ... @@ -145,4 +145,18 @@ ActiveRecord::Schema.define(:version =&gt; 20111220190817) do
145 145 t.integer "project_access", :default => 0, :null => false
146 146 end
147 147  
  148 + create_table "web_hook_urls", :force => true do |t|
  149 + t.string "url"
  150 + t.integer "project_id"
  151 + t.datetime "created_at"
  152 + t.datetime "updated_at"
  153 + end
  154 +
  155 + create_table "web_hooks", :force => true do |t|
  156 + t.string "url"
  157 + t.integer "project_id"
  158 + t.datetime "created_at"
  159 + t.datetime "updated_at"
  160 + end
  161 +
148 162 end
... ...
lib/gitlabhq/gitolite.rb
... ... @@ -43,14 +43,14 @@ module Gitlabhq
43 43  
44 44 def destroy_project(project)
45 45 FileUtils.rm_rf(project.path_to_repo)
46   -
  46 +
47 47 ga_repo = ::Gitolite::GitoliteAdmin.new(File.join(@local_dir,'gitolite'))
48 48 conf = ga_repo.config
49 49 conf.rm_repo(project.path)
50 50 ga_repo.save
51 51 end
52 52  
53   - #update or create
  53 + #update or create
54 54 def update_keys(user, key)
55 55 File.open(File.join(@local_dir, 'gitolite/keydir',"#{user}.pub"), 'w') {|f| f.write(key.gsub(/\n/,'')) }
56 56 end
... ...
lib/post-receive-hook 0 → 100755
... ... @@ -0,0 +1,12 @@
  1 +#!/bin/bash
  2 +
  3 +# This file was placed here by Gitlab. It makes sure that your pushed commits
  4 +# will be processed properly.
  5 +
  6 +while read oldrev newrev ref
  7 +do
  8 + # For every branch or tag that was pushed, create a Resque job in redis.
  9 + pwd=`pwd`
  10 + reponame=`basename "$pwd" | cut -d. -f1`
  11 + env -i redis-cli rpush "resque:queue:post-receive" "{\"class\":\"PostReceive\",\"args\":[\"$reponame\",\"$oldrev\",\"$newrev\",\"$ref\"]}" > /dev/null 2>&1
  12 +done
... ...
lib/tasks/resque.rake 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require 'resque/tasks'
... ...
spec/factories.rb
... ... @@ -55,3 +55,7 @@ Factory.add(:key, Key) do |obj|
55 55 obj.title = "Example key"
56 56 obj.key = File.read(File.join(Rails.root, "db", "pkey.example"))
57 57 end
  58 +
  59 +Factory.add(:web_hook, WebHook) do |obj|
  60 + obj.url = Faker::Internet.url
  61 +end
... ...
spec/models/project_spec.rb
... ... @@ -7,6 +7,7 @@ describe Project do
7 7 it { should have_many(:issues) }
8 8 it { should have_many(:notes) }
9 9 it { should have_many(:snippets) }
  10 + it { should have_many(:web_hooks).dependent(:destroy) }
10 11 end
11 12  
12 13 describe "Validation" do
... ... @@ -33,6 +34,7 @@ describe Project do
33 34 it { should respond_to(:repo) }
34 35 it { should respond_to(:tags) }
35 36 it { should respond_to(:commit) }
  37 + it { should respond_to(:commits_between) }
36 38 end
37 39  
38 40 it "should not allow 'gitolite-admin' as repo name" do
... ... @@ -50,6 +52,11 @@ describe Project do
50 52 project.path_to_repo.should == File.join(Rails.root, "tmp", "tests", "somewhere")
51 53 end
52 54  
  55 + it "returns the full web URL for this repo" do
  56 + project = Project.new(:code => "somewhere")
  57 + project.web_url.should == "#{GIT_HOST['host']}/somewhere"
  58 + end
  59 +
53 60 describe :valid_repo? do
54 61 it "should be valid repo" do
55 62 project = Factory :project
... ... @@ -62,6 +69,106 @@ describe Project do
62 69 end
63 70 end
64 71  
  72 + describe "web hooks" do
  73 + let(:project) { Factory :project }
  74 +
  75 + context "with no web hooks" do
  76 + it "raises no errors" do
  77 + lambda {
  78 + project.execute_web_hooks('oldrev', 'newrev', 'ref')
  79 + }.should_not raise_error
  80 + end
  81 + end
  82 +
  83 + context "with web hooks" do
  84 + before do
  85 + @webhook = Factory(:web_hook)
  86 + @webhook_2 = Factory(:web_hook)
  87 + project.web_hooks << [@webhook, @webhook_2]
  88 + end
  89 +
  90 + it "executes multiple web hook" do
  91 + @webhook.should_receive(:execute).once
  92 + @webhook_2.should_receive(:execute).once
  93 +
  94 + project.execute_web_hooks('oldrev', 'newrev', 'refs/heads/master')
  95 + end
  96 + end
  97 +
  98 + context "does not execute web hooks" do
  99 + before do
  100 + @webhook = Factory(:web_hook)
  101 + project.web_hooks << [@webhook]
  102 + end
  103 +
  104 + it "when pushing a branch for the first time" do
  105 + @webhook.should_not_receive(:execute)
  106 + project.execute_web_hooks('00000000000000000000000000000000', 'newrev', 'refs/heads/master')
  107 + end
  108 +
  109 + it "when pushing tags" do
  110 + @webhook.should_not_receive(:execute)
  111 + project.execute_web_hooks('oldrev', 'newrev', 'refs/tags/v1.0.0')
  112 + end
  113 + end
  114 +
  115 + context "when pushing new branches" do
  116 +
  117 + end
  118 +
  119 + context "when gathering commit data" do
  120 + before do
  121 + @oldrev, @newrev, @ref = project.fresh_commits(2).last.sha, project.fresh_commits(2).first.sha, 'refs/heads/master'
  122 + @commit = project.fresh_commits(2).first
  123 +
  124 + # Fill nil/empty attributes
  125 + project.description = "This is a description"
  126 +
  127 + @data = project.web_hook_data(@oldrev, @newrev, @ref)
  128 + end
  129 +
  130 + subject { @data }
  131 +
  132 + it { should include(before: @oldrev) }
  133 + it { should include(after: @newrev) }
  134 + it { should include(ref: @ref) }
  135 +
  136 + context "with repository data" do
  137 + subject { @data[:repository] }
  138 +
  139 + it { should include(name: project.name) }
  140 + it { should include(url: project.web_url) }
  141 + it { should include(description: project.description) }
  142 + it { should include(homepage: project.web_url) }
  143 + it { should include(private: project.private?) }
  144 + end
  145 +
  146 + context "with commits" do
  147 + subject { @data[:commits] }
  148 +
  149 + it { should be_an(Array) }
  150 + it { should have(1).element }
  151 +
  152 + context "the commit" do
  153 + subject { @data[:commits].first }
  154 +
  155 + it { should include(id: @commit.id) }
  156 + it { should include(message: @commit.safe_message) }
  157 + it { should include(timestamp: @commit.date.xmlschema) }
  158 + it { should include(url: "http://localhost/#{project.code}/commits/#{@commit.id}") }
  159 +
  160 + context "with a author" do
  161 + subject { @data[:commits].first[:author] }
  162 +
  163 + it { should include(name: @commit.author_name) }
  164 + it { should include(email: @commit.author_email) }
  165 + end
  166 + end
  167 + end
  168 +
  169 + end
  170 + end
  171 +
65 172 describe "updates" do
66 173 let(:project) { Factory :project }
67 174  
... ... @@ -107,6 +214,21 @@ describe Project do
107 214 it { project.fresh_commits.last.id.should == "0dac878dbfe0b9c6104a87d65fe999149a8d862c" }
108 215 end
109 216  
  217 + describe "commits_between" do
  218 + let(:project) { Factory :project }
  219 +
  220 + subject do
  221 + commits = project.commits_between("a6d1d4aca0c85816ddfd27d93773f43a31395033",
  222 + "2fb376f61875b58bceee0492e270e9c805294b1a")
  223 + commits.map { |c| c.id }
  224 + end
  225 +
  226 + it { should have(2).elements }
  227 + it { should include("2fb376f61875b58bceee0492e270e9c805294b1a") }
  228 + it { should include("4571e226fbcd7be1af16e9fa1e13b7ac003bebdf") }
  229 + it { should_not include("a6d1d4aca0c85816ddfd27d93773f43a31395033") }
  230 + end
  231 +
110 232 describe "Git methods" do
111 233 let(:project) { Factory :project }
112 234  
... ...
spec/models/web_hook_spec.rb 0 → 100644
... ... @@ -0,0 +1,54 @@
  1 +require 'spec_helper'
  2 +
  3 +describe WebHook do
  4 + describe "Associations" do
  5 + it { should belong_to :project }
  6 + end
  7 +
  8 + describe "Validations" do
  9 + it { should validate_presence_of(:url) }
  10 +
  11 + context "url format" do
  12 + it { should allow_value("http://example.com").for(:url) }
  13 + it { should allow_value("https://excample.com").for(:url) }
  14 + it { should allow_value("http://test.com/api").for(:url) }
  15 + it { should allow_value("http://test.com/api?key=abc").for(:url) }
  16 + it { should allow_value("http://test.com/api?key=abc&type=def").for(:url) }
  17 +
  18 + it { should_not allow_value("example.com").for(:url) }
  19 + it { should_not allow_value("ftp://example.com").for(:url) }
  20 + it { should_not allow_value("herp-and-derp").for(:url) }
  21 + end
  22 + end
  23 +
  24 + describe "execute" do
  25 + before(:each) do
  26 + @webhook = Factory :web_hook
  27 + @project = Factory :project
  28 + @project.web_hooks << [@webhook]
  29 + @data = { before: 'oldrev', after: 'newrev', ref: 'ref'}
  30 +
  31 + WebMock.stub_request(:post, @webhook.url)
  32 + end
  33 +
  34 + it "POSTs to the web hook URL" do
  35 + @webhook.execute(@data)
  36 + WebMock.should have_requested(:post, @webhook.url).once
  37 + end
  38 +
  39 + it "POSTs the data as JSON" do
  40 + json = @data.to_json
  41 +
  42 + @webhook.execute(@data)
  43 + WebMock.should have_requested(:post, @webhook.url).with(body: json).once
  44 + end
  45 +
  46 + it "catches exceptions" do
  47 + WebHook.should_receive(:post).and_raise("Some HTTP Post error")
  48 +
  49 + lambda {
  50 + @webhook.execute(@data)
  51 + }.should_not raise_error
  52 + end
  53 + end
  54 +end
... ...
spec/requests/projects_tree_perfomance_spec.rb
... ... @@ -9,7 +9,6 @@ describe &quot;Projects&quot; do
9 9 before do
10 10 @project = Factory :project
11 11 @project.add_access(@user, :read)
12   -
13 12 end
14 13  
15 14 it "should be fast" do
... ...
spec/spec_helper.rb
... ... @@ -8,6 +8,7 @@ require &#39;rspec/rails&#39;
8 8 require 'capybara/rails'
9 9 require 'capybara/rspec'
10 10 require 'capybara/dsl'
  11 +require 'webmock/rspec'
11 12 require 'factories'
12 13 require 'monkeypatch'
13 14  
... ... @@ -48,6 +49,8 @@ RSpec.configure do |config|
48 49 end
49 50  
50 51 DatabaseCleaner.start
  52 +
  53 + WebMock.disable_net_connect!(allow_localhost: true)
51 54 end
52 55  
53 56 config.after do
... ...
spec/workers/post_receive_spec.rb 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +require 'spec_helper'
  2 +
  3 +describe PostReceive do
  4 +
  5 + context "as a resque worker" do
  6 + it "reponds to #perform" do
  7 + PostReceive.should respond_to(:perform)
  8 + end
  9 + end
  10 +
  11 + context "web hooks" do
  12 + let(:project) { Factory :project }
  13 +
  14 + it "it retrieves the correct project" do
  15 + Project.should_receive(:find_by_path).with(project.path)
  16 + PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master')
  17 + end
  18 +
  19 + it "asks the project to execute web hooks" do
  20 + Project.stub(find_by_path: project)
  21 + project.should_receive(:execute_web_hooks).with('sha-old', 'sha-new', 'refs/heads/master')
  22 +
  23 + PostReceive.perform(project.path, 'sha-old', 'sha-new', 'refs/heads/master')
  24 + end
  25 + end
  26 +end
... ...