defunkt-integrity 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +146 -0
- data/Rakefile +89 -0
- data/app.rb +249 -0
- data/bin/integrity +80 -0
- data/config/config.sample.ru +30 -0
- data/config/config.sample.yml +31 -0
- data/config/thin.sample.yml +14 -0
- data/integrity.gemspec +66 -0
- data/lib/integrity.rb +44 -0
- data/lib/integrity/build.rb +56 -0
- data/lib/integrity/builder.rb +42 -0
- data/lib/integrity/core_ext/object.rb +6 -0
- data/lib/integrity/core_ext/string.rb +5 -0
- data/lib/integrity/core_ext/time.rb +13 -0
- data/lib/integrity/notifier.rb +49 -0
- data/lib/integrity/notifier/base.rb +55 -0
- data/lib/integrity/project.rb +87 -0
- data/lib/integrity/scm.rb +22 -0
- data/lib/integrity/scm/git.rb +72 -0
- data/lib/integrity/scm/git/uri.rb +57 -0
- data/lib/integrity/version.rb +3 -0
- data/public/buttons.css +82 -0
- data/public/reset.css +7 -0
- data/public/spinner.gif +0 -0
- data/spec/form_field_matchers.rb +91 -0
- data/spec/spec_helper.rb +130 -0
- data/vendor/sinatra-hacks/lib/hacks.rb +49 -0
- data/views/build.haml +2 -0
- data/views/build_info.haml +22 -0
- data/views/home.haml +22 -0
- data/views/integrity.sass +391 -0
- data/views/layout.haml +25 -0
- data/views/new.haml +53 -0
- data/views/not_found.haml +31 -0
- data/views/notifier.haml +7 -0
- data/views/project.haml +36 -0
- data/views/unauthorized.haml +38 -0
- metadata +197 -0
data/README.markdown
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
Integrity
|
2
|
+
=========
|
3
|
+
|
4
|
+
Integrity is your friendly automated Continuous Integration server.
|
5
|
+
|
6
|
+
It's fully usable from within its web interface (backed by [Sinatra][]),
|
7
|
+
allowing you to add a project, set preferences for it (where's the code
|
8
|
+
repository, is it completely private or public, etc), and run the build command
|
9
|
+
from there.
|
10
|
+
|
11
|
+
It has been designed with ruby projects in mind, but any project that can be
|
12
|
+
tested in an unix-y fashion (with a command line tool that returns 0 on success
|
13
|
+
and non-zero on failure) works with it.
|
14
|
+
|
15
|
+
Getting Started
|
16
|
+
===============
|
17
|
+
|
18
|
+
Install the `integrity` gem from GitHub:
|
19
|
+
|
20
|
+
gem sources add http://gems.github.com
|
21
|
+
sudo gem install foca-integrity
|
22
|
+
|
23
|
+
In order to setup Integrity, run the following command:
|
24
|
+
|
25
|
+
integrity install /path/to/my/app
|
26
|
+
|
27
|
+
Then browse to /path/to/my/app and edit the config files at your convenience.
|
28
|
+
The default configuration should be "good enough" in most cases, so you should
|
29
|
+
be pretty much ready to rock.
|
30
|
+
|
31
|
+
For deployment, we recommend [Thin][]. Provided with Integrity comes a thin.yml
|
32
|
+
file, so all you need to do after running `integrity install` should be
|
33
|
+
|
34
|
+
thin -C /path/to/my/app/thin.yml -R /path/to/my/app/config.ru start
|
35
|
+
|
36
|
+
And you should be up and running.
|
37
|
+
|
38
|
+
If you want automatic commit processing, you currently need to be using
|
39
|
+
[GitHub][]. Click the edit link on your GitHub project, and add an integrity
|
40
|
+
link that looks like the following to the `Post-Receive URL` field:
|
41
|
+
|
42
|
+
http://integrity.domain.tld/projectname/push
|
43
|
+
|
44
|
+
Receiving Notifications
|
45
|
+
=======================
|
46
|
+
|
47
|
+
If you want to be notified after each build, you need to install our notifiers.
|
48
|
+
For example, in order to receive an email after each build, install:
|
49
|
+
|
50
|
+
sudo gem install foca-integrity-email
|
51
|
+
|
52
|
+
And then edit `/path/to/my/app/config.ru` and add:
|
53
|
+
|
54
|
+
require "notifier/email"
|
55
|
+
|
56
|
+
After all the `require` lines.
|
57
|
+
|
58
|
+
Resources
|
59
|
+
========
|
60
|
+
|
61
|
+
We have a [Lighthouse account][lighthouse] where you can submit patches or
|
62
|
+
feature requests. Also, someone is usually around [#integrity][irc-channel] on
|
63
|
+
Freenode, so don't hesitate to stop by for ideas, help, patches or something.
|
64
|
+
|
65
|
+
Future plans
|
66
|
+
============
|
67
|
+
|
68
|
+
* [Twitter][]/[IRC][]/etc bots
|
69
|
+
* A sample generic post-receive-hook so you can run this from any git repo
|
70
|
+
* Better integration with GitHub
|
71
|
+
|
72
|
+
Development
|
73
|
+
===========
|
74
|
+
|
75
|
+
The code is stored in [GitHub][repo]. Feel free to fork, play with it, and send
|
76
|
+
a pull request afterwards.
|
77
|
+
|
78
|
+
In order to run the test suite you'll need a few more gems: [rspec][], [rcov][]
|
79
|
+
and [hpricot][]. With that installed running `rake` will run the specs and
|
80
|
+
ensure the code coverage stays high.
|
81
|
+
|
82
|
+
Thanks
|
83
|
+
======
|
84
|
+
|
85
|
+
Thanks to the fellowing people for their feedbacks, ideas and patches :
|
86
|
+
|
87
|
+
* [James Adam][james]
|
88
|
+
* [Elliott Cable][ec]
|
89
|
+
* [Corey Donohoe][atmos]
|
90
|
+
* [Kyle Hargraves][kyle]
|
91
|
+
* [Pier-Hugues Pellerin][ph]
|
92
|
+
* [Simon Rozet][sr]
|
93
|
+
* [Scott Taylor][scott]
|
94
|
+
|
95
|
+
[james]: http://github.com/lazyatom
|
96
|
+
[ec]: http://github.com/elliotcabble
|
97
|
+
[atmos]: http://github.com/atmos
|
98
|
+
[kyle]: http://github.com/pd
|
99
|
+
[ph]: http://github.com/ph
|
100
|
+
[sr]: http://purl.org/net/sr/
|
101
|
+
[scott]: http://github.com/smtlaissezfaire
|
102
|
+
|
103
|
+
License
|
104
|
+
=======
|
105
|
+
|
106
|
+
(The MIT License)
|
107
|
+
|
108
|
+
Copyright (c) 2008 [Nicolás Sanguinetti][foca], [entp][]
|
109
|
+
|
110
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
111
|
+
a copy of this software and associated documentation files (the
|
112
|
+
'Software'), to deal in the Software without restriction, including
|
113
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
114
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
115
|
+
permit persons to whom the Software is furnished to do so, subject to
|
116
|
+
the following conditions:
|
117
|
+
|
118
|
+
The above copyright notice and this permission notice shall be
|
119
|
+
included in all copies or substantial portions of the Software.
|
120
|
+
|
121
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
122
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
123
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
124
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
125
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
126
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
127
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
128
|
+
|
129
|
+
[Sinatra]: http://sinatrarb.com
|
130
|
+
[git]: http://git.or.cz
|
131
|
+
[svn]: http://subversion.tigris.org
|
132
|
+
[Twitter]: http://twitter.com
|
133
|
+
[IRC]: http://wikipedia.org/wiki/IRC
|
134
|
+
[entp]: http://entp.com
|
135
|
+
[GitHub]: http://github.com
|
136
|
+
[Thin]: http://code.macournoyer.com/thin/
|
137
|
+
|
138
|
+
[rspec]: http://rspec.info
|
139
|
+
[rcov]: http://eigenclass.org/hiki.rb?rcov
|
140
|
+
[hpricot]: http://code.whytheluckystiff.net/hpricot
|
141
|
+
|
142
|
+
[repo]: http://github.com/foca/integrity
|
143
|
+
[lighthouse]: http://integrity.lighthouseapp.com/projects/14308-integrity
|
144
|
+
[irc-channel]: irc://irc.freenode.net/integrity
|
145
|
+
|
146
|
+
[foca]: http://nicolassanguinetti.info/
|
data/Rakefile
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/lib/integrity"
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
require 'spec/rake/verify_rcov'
|
4
|
+
|
5
|
+
task :default => ["spec:coverage", "spec:coverage:verify"]
|
6
|
+
|
7
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
8
|
+
t.spec_opts = ["--color", "--format", "progress"]
|
9
|
+
t.spec_files = Dir['spec/**/*_spec.rb'].sort
|
10
|
+
t.libs = ['lib']
|
11
|
+
t.rcov = false
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :spec do
|
15
|
+
Spec::Rake::SpecTask.new(:coverage) do |t|
|
16
|
+
t.spec_opts = ["--color", "--format", "progress"]
|
17
|
+
t.spec_files = Dir['spec/**/*_spec.rb'].sort
|
18
|
+
t.libs = ['lib']
|
19
|
+
t.rcov = true
|
20
|
+
t.rcov_opts = ['--exclude-only', '".*"', '--include-file', '^lib']
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :coverage do
|
24
|
+
RCov::VerifyTask.new(:verify) do |t|
|
25
|
+
t.threshold = 100
|
26
|
+
t.index_html = "coverage" / 'index.html'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
namespace :db do
|
32
|
+
desc "Setup connection."
|
33
|
+
task :connect do
|
34
|
+
config = File.expand_path(ENV['CONFIG']) if ENV['CONFIG']
|
35
|
+
config = Integrity.root / 'config.yml' if File.exists?(Integrity.root / 'config.yml')
|
36
|
+
Integrity.config = config if config
|
37
|
+
Integrity.new
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Automigrate the database"
|
41
|
+
task :migrate => :connect do
|
42
|
+
require "project"
|
43
|
+
require "build"
|
44
|
+
require "notifier"
|
45
|
+
DataMapper.auto_migrate!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
namespace :gem do
|
50
|
+
desc "Generate the gemspec at the root dir"
|
51
|
+
task :gemspec do
|
52
|
+
files = `git ls-files`.split("\n").reject {|f| f =~ %r(^spec) || f =~ %r(^vendor/rspec) || f =~ /^\.git/ }
|
53
|
+
files += %w(spec/spec_helper.rb spec/form_field_matchers.rb)
|
54
|
+
|
55
|
+
gemspec = <<-GEM
|
56
|
+
Gem::Specification.new do |s|
|
57
|
+
s.name = 'integrity'
|
58
|
+
s.version = '#{Integrity::VERSION}'
|
59
|
+
s.date = '#{Date.today.to_s}'
|
60
|
+
s.summary = 'The easy and fun Continuous Integration server'
|
61
|
+
s.description = 'Your Friendly Continuous Integration server. Easy, fun and painless!'
|
62
|
+
s.homepage = 'http://integrityapp.com'
|
63
|
+
s.rubyforge_project = 'integrity'
|
64
|
+
s.email = 'contacto@nicolassanguinetti.info'
|
65
|
+
s.authors = ['Nicolás Sanguinetti', 'Simon Rozet']
|
66
|
+
s.has_rdoc = false
|
67
|
+
s.executables = ['integrity']
|
68
|
+
s.files = %w(
|
69
|
+
#{files.join("\n" + " " * 26)}
|
70
|
+
)
|
71
|
+
|
72
|
+
s.add_dependency 'sinatra', ['>= 0.3.2']
|
73
|
+
s.add_dependency 'dm-core', ['>= 0.9.5']
|
74
|
+
s.add_dependency 'dm-validations', ['>= 0.9.5']
|
75
|
+
s.add_dependency 'dm-types', ['>= 0.9.5']
|
76
|
+
s.add_dependency 'dm-timestamps', ['>= 0.9.5']
|
77
|
+
s.add_dependency 'dm-aggregates', ['>= 0.9.5']
|
78
|
+
s.add_dependency 'data_objects', ['>= 0.9.5']
|
79
|
+
s.add_dependency 'do_sqlite3', ['>= 0.9.5']
|
80
|
+
s.add_dependency 'json'
|
81
|
+
s.add_dependency 'foca-sinatra-diddies', ['>= 0.0.2']
|
82
|
+
s.add_dependency 'rspec_hpricot_matchers'
|
83
|
+
s.add_dependency 'thor'
|
84
|
+
end
|
85
|
+
GEM
|
86
|
+
|
87
|
+
File.open(Integrity.root / "integrity.gemspec", "w") {|f| f.puts gemspec }
|
88
|
+
end
|
89
|
+
end
|
data/app.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/lib/integrity'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'diddies'
|
4
|
+
require 'hacks'
|
5
|
+
|
6
|
+
set :root, Integrity.root
|
7
|
+
set :public, Integrity.root / "public"
|
8
|
+
set :views, Integrity.root / "views"
|
9
|
+
|
10
|
+
enable :sessions
|
11
|
+
|
12
|
+
include Integrity
|
13
|
+
|
14
|
+
configure do
|
15
|
+
config = Integrity.root / "config.yml"
|
16
|
+
Integrity.config = config if File.exists? config
|
17
|
+
Integrity.new
|
18
|
+
end
|
19
|
+
|
20
|
+
not_found do
|
21
|
+
status 404
|
22
|
+
show :not_found, :title => "lost, are we?"
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
# The browser only sends http auth data for requests that are explicitly
|
27
|
+
# required to do so. This way we get the real values of +#logged_in?+ and
|
28
|
+
# +#current_user+
|
29
|
+
login_required if session[:user]
|
30
|
+
end
|
31
|
+
|
32
|
+
get "/" do
|
33
|
+
@projects = Project.all(authorized? ? {} : { :public => true })
|
34
|
+
show :home, :title => "projects"
|
35
|
+
end
|
36
|
+
|
37
|
+
get "/login" do
|
38
|
+
login_required
|
39
|
+
session[:user] = current_user
|
40
|
+
redirect "/"
|
41
|
+
end
|
42
|
+
|
43
|
+
get "/new" do
|
44
|
+
login_required
|
45
|
+
|
46
|
+
@project = Project.new
|
47
|
+
show :new, :title => ["projects", "new project"]
|
48
|
+
end
|
49
|
+
|
50
|
+
post "/" do
|
51
|
+
login_required
|
52
|
+
|
53
|
+
@project = Project.new(params[:project_data])
|
54
|
+
if @project.save
|
55
|
+
@project.enable_notifiers(params["enabled_notifiers[]"], params["notifiers"])
|
56
|
+
redirect project_url(@project)
|
57
|
+
else
|
58
|
+
show :new, :title => ["projects", "new project"]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
get "/:project" do
|
63
|
+
login_required unless current_project.public?
|
64
|
+
show :project, :title => ["projects", current_project.permalink]
|
65
|
+
end
|
66
|
+
|
67
|
+
put "/:project" do
|
68
|
+
login_required
|
69
|
+
|
70
|
+
if current_project.update_attributes(params[:project_data])
|
71
|
+
current_project.enable_notifiers(params["enabled_notifiers[]"], params["notifiers"])
|
72
|
+
redirect project_url(current_project)
|
73
|
+
else
|
74
|
+
show :new, :title => ["projects", current_project.permalink, "edit"]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
delete "/:project" do
|
79
|
+
login_required
|
80
|
+
|
81
|
+
current_project.destroy
|
82
|
+
redirect "/"
|
83
|
+
end
|
84
|
+
|
85
|
+
get "/:project/edit" do
|
86
|
+
login_required
|
87
|
+
|
88
|
+
show :new, :title => ["projects", current_project.permalink, "edit"]
|
89
|
+
end
|
90
|
+
|
91
|
+
post "/:project/push" do
|
92
|
+
content_type 'text/plain'
|
93
|
+
|
94
|
+
begin
|
95
|
+
payload = JSON.parse(params[:payload] || "")
|
96
|
+
payload['commits'].reverse.each do |commit|
|
97
|
+
current_project.build(commit['id']) if payload['ref'] =~ /#{current_project.branch}/
|
98
|
+
end
|
99
|
+
'Thanks, build started.'
|
100
|
+
rescue JSON::ParserError => exception
|
101
|
+
invalid_payload!(exception.to_s)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
post "/:project/builds" do
|
106
|
+
login_required
|
107
|
+
|
108
|
+
current_project.build
|
109
|
+
redirect project_url(@project)
|
110
|
+
end
|
111
|
+
|
112
|
+
get '/:project/builds/:build' do
|
113
|
+
login_required unless current_project.public?
|
114
|
+
show :build, :title => ["projects", current_project.permalink, current_build.short_commit_identifier]
|
115
|
+
end
|
116
|
+
|
117
|
+
get "/integrity.css" do
|
118
|
+
header "Content-Type" => "text/css; charset=utf-8"
|
119
|
+
sass :integrity
|
120
|
+
end
|
121
|
+
|
122
|
+
helpers do
|
123
|
+
include Rack::Utils
|
124
|
+
include Sinatra::Authorization
|
125
|
+
alias_method :h, :escape_html
|
126
|
+
|
127
|
+
def authorization_realm
|
128
|
+
"Integrity"
|
129
|
+
end
|
130
|
+
|
131
|
+
def authorized?
|
132
|
+
return true unless Integrity.config[:use_basic_auth]
|
133
|
+
!!request.env['REMOTE_USER']
|
134
|
+
end
|
135
|
+
|
136
|
+
def authorize(user, password)
|
137
|
+
if Integrity.config[:hash_admin_password]
|
138
|
+
password = Digest::SHA1.hexdigest(password)
|
139
|
+
end
|
140
|
+
|
141
|
+
!Integrity.config[:use_basic_auth] ||
|
142
|
+
(Integrity.config[:admin_username] == user &&
|
143
|
+
Integrity.config[:admin_password] == password)
|
144
|
+
end
|
145
|
+
|
146
|
+
def unauthorized!(realm=authorization_realm)
|
147
|
+
header 'WWW-Authenticate' => %(Basic realm="#{realm}")
|
148
|
+
throw :halt, [401, show(:unauthorized, :title => "incorrect credentials")]
|
149
|
+
end
|
150
|
+
|
151
|
+
def invalid_payload!(msg=nil)
|
152
|
+
throw :halt, [422, msg || 'No payload given']
|
153
|
+
end
|
154
|
+
|
155
|
+
def current_project
|
156
|
+
@project ||= Project.first(:permalink => params[:project]) or raise Sinatra::NotFound
|
157
|
+
end
|
158
|
+
|
159
|
+
def current_build
|
160
|
+
@build ||= current_project.builds.first(:commit_identifier => params[:build]) or raise Sinatra::NotFound
|
161
|
+
end
|
162
|
+
|
163
|
+
def show(view, options={})
|
164
|
+
@title = breadcrumbs(*options[:title])
|
165
|
+
haml view
|
166
|
+
end
|
167
|
+
|
168
|
+
def pages
|
169
|
+
@pages ||= [["projects", "/"], ["new project", "/new"]]
|
170
|
+
end
|
171
|
+
|
172
|
+
def breadcrumbs(*crumbs)
|
173
|
+
crumbs[0..-2].map do |crumb|
|
174
|
+
if page_data = pages.detect {|c| c.first == crumb }
|
175
|
+
%Q(<a href="#{page_data.last}">#{page_data.first}</a>)
|
176
|
+
elsif @project && @project.permalink == crumb
|
177
|
+
%Q(<a href="#{project_url(@project)}">#{@project.permalink}</a>)
|
178
|
+
end
|
179
|
+
end + [crumbs.last]
|
180
|
+
end
|
181
|
+
|
182
|
+
def cycle(*values)
|
183
|
+
@cycles ||= {}
|
184
|
+
@cycles[values] ||= -1 # first value returned is 0
|
185
|
+
next_value = @cycles[values] = (@cycles[values] + 1) % values.size
|
186
|
+
values[next_value]
|
187
|
+
end
|
188
|
+
|
189
|
+
def project_url(project, *path)
|
190
|
+
"/" << [project.permalink, *path].join("/")
|
191
|
+
end
|
192
|
+
|
193
|
+
def push_url_for(project)
|
194
|
+
Addressable::URI.parse(Integrity.config[:base_uri]).join("#{project_url(project)}/push").to_s
|
195
|
+
end
|
196
|
+
|
197
|
+
def build_url(build)
|
198
|
+
"/#{build.project.permalink}/builds/#{build.commit_identifier}"
|
199
|
+
end
|
200
|
+
|
201
|
+
def filter_attributes_of(model)
|
202
|
+
valid = model.properties.collect {|p| p.name.to_s }
|
203
|
+
Hash[*params.dup.select {|k,_| valid.include?(k) }.flatten]
|
204
|
+
end
|
205
|
+
|
206
|
+
def errors_on(object, field)
|
207
|
+
return "" unless errors = object.errors.on(field)
|
208
|
+
errors.map {|e| e.gsub(/#{field} /i, "") }.join(", ")
|
209
|
+
end
|
210
|
+
|
211
|
+
def error_class(object, field)
|
212
|
+
object.errors.on(field).nil? ? "" : "with_errors"
|
213
|
+
end
|
214
|
+
|
215
|
+
def checkbox(name, condition, extras={})
|
216
|
+
attrs = { :name => name, :type => "checkbox" }.merge(condition ? { :checked => "checked" } : {})
|
217
|
+
attrs.merge(extras)
|
218
|
+
end
|
219
|
+
|
220
|
+
def bash_color_codes(string)
|
221
|
+
string.gsub("\e[0m", '</span>').
|
222
|
+
gsub("\e[31m", '<span class="color31">').
|
223
|
+
gsub("\e[32m", '<span class="color32">').
|
224
|
+
gsub("\e[33m", '<span class="color33">').
|
225
|
+
gsub("\e[34m", '<span class="color34">').
|
226
|
+
gsub("\e[35m", '<span class="color35">').
|
227
|
+
gsub("\e[36m", '<span class="color36">').
|
228
|
+
gsub("\e[37m", '<span class="color37">')
|
229
|
+
end
|
230
|
+
|
231
|
+
def pretty_date(date_time)
|
232
|
+
today = Date.today
|
233
|
+
if date_time.day == today.day && date_time.month == today.month && date_time.year == today.year
|
234
|
+
"today"
|
235
|
+
elsif date_time.day == today.day - 1 && date_time.month == today.month && date_time.year == today.year
|
236
|
+
"yesterday"
|
237
|
+
else
|
238
|
+
date_time.strftime("on %b %d%o")
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def notifier_form(notifier)
|
243
|
+
haml(notifier.to_haml, :layout => :notifier, :locals => {
|
244
|
+
:config => current_project.config_for(notifier),
|
245
|
+
:notifier => "#{notifier.to_s.split(/::/).last}",
|
246
|
+
:enabled => current_project.notifies?(notifier)
|
247
|
+
})
|
248
|
+
end
|
249
|
+
end
|