integrity-integrity 0.1.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/CHANGES +48 -0
- data/README.md +82 -0
- data/Rakefile +58 -0
- data/bin/integrity +4 -0
- data/config/config.sample.ru +21 -0
- data/config/config.sample.yml +45 -0
- data/config/heroku/.gems +1 -0
- data/config/heroku/Rakefile +6 -0
- data/config/heroku/config.ru +7 -0
- data/config/heroku/integrity-config.rb +15 -0
- data/config/thin.sample.yml +13 -0
- data/integrity.gemspec +136 -0
- data/lib/integrity.rb +77 -0
- data/lib/integrity/app.rb +138 -0
- data/lib/integrity/author.rb +39 -0
- data/lib/integrity/build.rb +52 -0
- data/lib/integrity/commit.rb +60 -0
- data/lib/integrity/core_ext/object.rb +6 -0
- data/lib/integrity/helpers.rb +15 -0
- data/lib/integrity/helpers/authorization.rb +33 -0
- data/lib/integrity/helpers/breadcrumbs.rb +20 -0
- data/lib/integrity/helpers/pretty_output.rb +45 -0
- data/lib/integrity/helpers/rendering.rb +49 -0
- data/lib/integrity/helpers/resources.rb +19 -0
- data/lib/integrity/helpers/urls.rb +59 -0
- data/lib/integrity/installer.rb +145 -0
- data/lib/integrity/migrations.rb +151 -0
- data/lib/integrity/notifier.rb +44 -0
- data/lib/integrity/notifier/base.rb +74 -0
- data/lib/integrity/notifier/test.rb +52 -0
- data/lib/integrity/notifier/test/fixtures.rb +108 -0
- data/lib/integrity/notifier/test/hpricot_matcher.rb +38 -0
- data/lib/integrity/project.rb +93 -0
- data/lib/integrity/project/notifiers.rb +31 -0
- data/lib/integrity/project/push.rb +43 -0
- data/lib/integrity/project_builder.rb +56 -0
- data/lib/integrity/scm.rb +19 -0
- data/lib/integrity/scm/git.rb +84 -0
- data/lib/integrity/scm/git/uri.rb +57 -0
- data/public/buttons.css +82 -0
- data/public/reset.css +7 -0
- data/public/spinner.gif +0 -0
- data/test/acceptance/api_test.rb +97 -0
- data/test/acceptance/browse_project_builds_test.rb +65 -0
- data/test/acceptance/browse_project_test.rb +99 -0
- data/test/acceptance/build_notifications_test.rb +114 -0
- data/test/acceptance/create_project_test.rb +97 -0
- data/test/acceptance/delete_project_test.rb +53 -0
- data/test/acceptance/edit_project_test.rb +117 -0
- data/test/acceptance/error_page_test.rb +21 -0
- data/test/acceptance/installer_test.rb +81 -0
- data/test/acceptance/manual_build_project_test.rb +82 -0
- data/test/acceptance/not_found_page_test.rb +29 -0
- data/test/acceptance/project_syndication_test.rb +30 -0
- data/test/acceptance/stylesheet_test.rb +26 -0
- data/test/acceptance/unauthorized_page_test.rb +20 -0
- data/test/helpers.rb +75 -0
- data/test/helpers/acceptance.rb +82 -0
- data/test/helpers/acceptance/email_notifier.rb +52 -0
- data/test/helpers/acceptance/git_helper.rb +99 -0
- data/test/helpers/acceptance/notifier_helper.rb +47 -0
- data/test/helpers/acceptance/textfile_notifier.rb +26 -0
- data/test/helpers/expectations.rb +4 -0
- data/test/helpers/expectations/be_a.rb +23 -0
- data/test/helpers/expectations/change.rb +90 -0
- data/test/helpers/expectations/have.rb +105 -0
- data/test/helpers/expectations/predicates.rb +37 -0
- data/test/helpers/initial_migration_fixture.sql +44 -0
- data/test/unit/build_test.rb +72 -0
- data/test/unit/commit_test.rb +66 -0
- data/test/unit/helpers_test.rb +103 -0
- data/test/unit/integrity_test.rb +35 -0
- data/test/unit/migrations_test.rb +57 -0
- data/test/unit/notifier/base_test.rb +43 -0
- data/test/unit/notifier_test.rb +96 -0
- data/test/unit/project_builder_test.rb +118 -0
- data/test/unit/project_test.rb +344 -0
- data/test/unit/scm_test.rb +54 -0
- data/views/_commit_info.haml +24 -0
- data/views/build.haml +2 -0
- data/views/error.haml +37 -0
- data/views/home.haml +21 -0
- data/views/integrity.sass +400 -0
- data/views/layout.haml +29 -0
- data/views/new.haml +50 -0
- data/views/not_found.haml +31 -0
- data/views/notifier.haml +7 -0
- data/views/project.builder +21 -0
- data/views/project.haml +30 -0
- data/views/unauthorized.haml +38 -0
- metadata +323 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/notifier/base"
|
2
|
+
|
3
|
+
module Integrity
|
4
|
+
class Notifier
|
5
|
+
include DataMapper::Resource
|
6
|
+
|
7
|
+
property :id, Integer, :serial => true
|
8
|
+
property :name, String, :nullable => false
|
9
|
+
property :enabled, Boolean, :nullable => false, :default => false
|
10
|
+
property :config, Yaml, :nullable => false, :lazy => false
|
11
|
+
|
12
|
+
belongs_to :project, :class_name => "Integrity::Project",
|
13
|
+
:child_key => [:project_id]
|
14
|
+
|
15
|
+
validates_is_unique :name, :scope => :project_id
|
16
|
+
validates_present :project_id
|
17
|
+
|
18
|
+
def self.available
|
19
|
+
@@_notifiers ||= {}
|
20
|
+
@@_notifiers
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.register(klass)
|
24
|
+
raise ArgumentError unless valid?(klass)
|
25
|
+
|
26
|
+
available[klass.to_s.split(":").last] = klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.valid?(notifier)
|
30
|
+
notifier.respond_to?(:to_haml) && notifier.respond_to?(:notify_of_build) &&
|
31
|
+
notifier != Notifier::Base
|
32
|
+
end
|
33
|
+
private_class_method :valid?
|
34
|
+
|
35
|
+
def notify_of_build(build)
|
36
|
+
to_const.notify_of_build(build, config)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def to_const
|
41
|
+
self.class.available[name]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Integrity
|
2
|
+
class Notifier
|
3
|
+
class Base
|
4
|
+
def self.notify_of_build(build, config)
|
5
|
+
Integrity.log "Notifying of build #{build.commit.short_identifier} using the #{to_s} notifier"
|
6
|
+
Timeout.timeout(8) { new(build.commit, config).deliver! }
|
7
|
+
rescue Timeout::Error
|
8
|
+
Integrity.log "#{notifier.name} notifier timed out"
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.to_haml
|
13
|
+
raise NotImplementedError, "you need to implement this method in your notifier"
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :commit
|
17
|
+
|
18
|
+
def initialize(commit, config)
|
19
|
+
@commit = commit
|
20
|
+
@config = config
|
21
|
+
end
|
22
|
+
|
23
|
+
def build
|
24
|
+
warn "Notifier::Base#build is deprecated, use Notifier::Base#commit instead (#{caller[0]})"
|
25
|
+
commit
|
26
|
+
end
|
27
|
+
|
28
|
+
def deliver!
|
29
|
+
raise NotImplementedError, "you need to implement this method in your notifier"
|
30
|
+
end
|
31
|
+
|
32
|
+
def short_message
|
33
|
+
commit.human_readable_status
|
34
|
+
end
|
35
|
+
|
36
|
+
def full_message
|
37
|
+
<<-EOM
|
38
|
+
"Build #{commit.identifier} #{commit.successful? ? "was successful" : "failed"}"
|
39
|
+
|
40
|
+
Commit Message: #{commit.message}
|
41
|
+
Commit Date: #{commit.committed_at}
|
42
|
+
Commit Author: #{commit.author.name}
|
43
|
+
|
44
|
+
Link: #{commit_url}
|
45
|
+
|
46
|
+
Build Output:
|
47
|
+
|
48
|
+
#{stripped_commit_output}
|
49
|
+
EOM
|
50
|
+
end
|
51
|
+
|
52
|
+
def commit_url
|
53
|
+
raise if Integrity.config[:base_uri].nil?
|
54
|
+
Integrity.config[:base_uri] / commit.project.permalink / "commits" / commit.identifier
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_url
|
58
|
+
warn "Notifier::Base#build_url is deprecated, use Notifier::Base#commit_url instead (#{caller[0]})"
|
59
|
+
commit_url
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def stripped_commit_output
|
65
|
+
commit.output.gsub("\e[0m", "").gsub(/\e\[3[1-7]m/, "")
|
66
|
+
end
|
67
|
+
|
68
|
+
def stripped_build_output
|
69
|
+
warn "Notifier::Base#stripped_build_output is deprecated, use Notifier::base#stripped_commit_output instead (#{caller[0]})"
|
70
|
+
stripped_commit_output
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../../integrity"
|
2
|
+
|
3
|
+
require "integrity/notifier/test/hpricot_matcher"
|
4
|
+
require "integrity/notifier/test/fixtures"
|
5
|
+
|
6
|
+
module Integrity
|
7
|
+
class Notifier
|
8
|
+
module Test
|
9
|
+
def setup_database
|
10
|
+
DataMapper.setup(:default, "sqlite3::memory:")
|
11
|
+
DataMapper.auto_migrate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(state=:successful)
|
15
|
+
Integrity::Build.gen(state)
|
16
|
+
end
|
17
|
+
|
18
|
+
def notifier_class
|
19
|
+
Integrity::Notifier.const_get(notifier)
|
20
|
+
end
|
21
|
+
|
22
|
+
def provides_option?(option, value=nil)
|
23
|
+
selector = "input##{notifier.downcase}_notifier_#{option}"
|
24
|
+
selector << "[@name='notifiers[#{notifier}][#{option}]']"
|
25
|
+
selector << "[@value='#{value}']" if value
|
26
|
+
|
27
|
+
form_have_tag?(selector, option => value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def provides_options(*options)
|
31
|
+
options.each { |option| assert_form_have_option(option) }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def form(config={})
|
36
|
+
Haml::Engine.new(notifier_class.to_haml).
|
37
|
+
render(OpenStruct.new(:config => config))
|
38
|
+
end
|
39
|
+
|
40
|
+
def form_have_tag?(selector, options={})
|
41
|
+
content = options.delete(:content)
|
42
|
+
have_tag?(form(options), selector, content)
|
43
|
+
end
|
44
|
+
|
45
|
+
def have_tag?(html, selector, content=nil)
|
46
|
+
matcher = HpricotMatcher.new(html)
|
47
|
+
assert_equal content, matcher.tag(selector) if content
|
48
|
+
matcher.tag(selector)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "dm-sweatshop"
|
2
|
+
|
3
|
+
include DataMapper::Sweatshop::Unique
|
4
|
+
|
5
|
+
class Array
|
6
|
+
def pick
|
7
|
+
self[rand(self.length)]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_notifier!(name)
|
12
|
+
klass = Class.new(Integrity::Notifier::Base) do
|
13
|
+
def self.to_haml; ""; end
|
14
|
+
def deliver!; nil; end
|
15
|
+
end
|
16
|
+
|
17
|
+
unless Integrity::Notifier.const_defined?(name)
|
18
|
+
Integrity::Notifier.const_set(name, klass)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Integrity::Project.fixture do
|
23
|
+
{ :name => (name = unique { /\w+/.gen }),
|
24
|
+
:uri => "git://github.com/#{/\w+/.gen}/#{name}.git",
|
25
|
+
:branch => ["master", "test-refactoring", "lh-34"].pick,
|
26
|
+
:command => ["rake", "make", "ant -buildfile test.xml"].pick,
|
27
|
+
:public => [true, false].pick,
|
28
|
+
:building => [true, false].pick }
|
29
|
+
end
|
30
|
+
|
31
|
+
Integrity::Project.fixture(:integrity) do
|
32
|
+
{ :name => "Integrity",
|
33
|
+
:uri => "git://github.com/foca/integrity.git",
|
34
|
+
:branch => "master",
|
35
|
+
:command => "rake",
|
36
|
+
:public => true,
|
37
|
+
:building => false }
|
38
|
+
end
|
39
|
+
|
40
|
+
Integrity::Project.fixture(:my_test_project) do
|
41
|
+
{ :name => "My Test Project",
|
42
|
+
:uri => File.dirname(__FILE__) + "/../../",
|
43
|
+
:branch => "master",
|
44
|
+
:command => "./test",
|
45
|
+
:public => true,
|
46
|
+
:building => false }
|
47
|
+
end
|
48
|
+
|
49
|
+
Integrity::Commit.fixture do
|
50
|
+
project = Integrity::Project.first || Integrity::Project.gen
|
51
|
+
|
52
|
+
{ :identifier => Digest::SHA1.hexdigest(/[:paragraph:]/.gen),
|
53
|
+
:message => /[:sentence:]/.gen,
|
54
|
+
:author => /\w+ \w+ <\w+@example.org>/.gen,
|
55
|
+
:committed_at => unique {|i| Time.mktime(2008, 12, 15, 18, (59 - i) % 60) },
|
56
|
+
:project_id => project.id }
|
57
|
+
end
|
58
|
+
|
59
|
+
Integrity::Commit.fixture(:successful) do
|
60
|
+
Integrity::Commit.generate_attributes.update(:build => Integrity::Build.gen(:successful))
|
61
|
+
end
|
62
|
+
|
63
|
+
Integrity::Commit.fixture(:failed) do
|
64
|
+
Integrity::Commit.generate_attributes.update(:build => Integrity::Build.gen(:failed))
|
65
|
+
end
|
66
|
+
|
67
|
+
Integrity::Commit.fixture(:pending) do
|
68
|
+
Integrity::Commit.generate_attributes.update(:build => Integrity::Build.gen(:pending))
|
69
|
+
end
|
70
|
+
|
71
|
+
Integrity::Build.fixture do
|
72
|
+
commit = Integrity::Commit.first || Integrity::Commit.gen
|
73
|
+
|
74
|
+
{ :output => /[:paragraph:]/.gen,
|
75
|
+
:successful => true,
|
76
|
+
:started_at => unique {|i| Time.mktime(2008, 12, 15, 18, i % 60) },
|
77
|
+
:created_at => unique {|i| Time.mktime(2008, 12, 15, 18, i % 60) },
|
78
|
+
:completed_at => unique {|i| Time.mktime(2008, 12, 15, 18, i % 60) },
|
79
|
+
:commit_id => commit.id }
|
80
|
+
end
|
81
|
+
|
82
|
+
Integrity::Build.fixture(:successful) do
|
83
|
+
Integrity::Build.generate_attributes.update(:successful => true)
|
84
|
+
end
|
85
|
+
|
86
|
+
Integrity::Build.fixture(:failed) do
|
87
|
+
Integrity::Build.generate_attributes.update(:successful => false)
|
88
|
+
end
|
89
|
+
|
90
|
+
Integrity::Build.fixture(:pending) do
|
91
|
+
Integrity::Build.generate_attributes.update(:successful => nil, :started_at => nil, :completed_at => nil)
|
92
|
+
end
|
93
|
+
|
94
|
+
Integrity::Notifier.fixture(:irc) do
|
95
|
+
create_notifier! "IRC"
|
96
|
+
|
97
|
+
{ :project => Integrity::Project.generate,
|
98
|
+
:name => "IRC",
|
99
|
+
:config => { :uri => "irc://irc.freenode.net/integrity" }}
|
100
|
+
end
|
101
|
+
|
102
|
+
Integrity::Notifier.fixture(:twitter) do
|
103
|
+
create_notifier! "Twitter"
|
104
|
+
|
105
|
+
{ :project => Integrity::Project.generate,
|
106
|
+
:name => "Twitter",
|
107
|
+
:config => { :email => "foo@example.org", :pass => "secret" }}
|
108
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "hpricot"
|
2
|
+
|
3
|
+
module Integrity
|
4
|
+
class Notifier
|
5
|
+
module Test
|
6
|
+
# Thanks Harry! http://gist.github.com/39960
|
7
|
+
class HpricotMatcher
|
8
|
+
def initialize(html)
|
9
|
+
@doc = Hpricot(html)
|
10
|
+
end
|
11
|
+
|
12
|
+
# elements('h1') returns a Hpricot::Elements object with all h1-tags.
|
13
|
+
def elements(selector)
|
14
|
+
@doc.search(selector)
|
15
|
+
end
|
16
|
+
|
17
|
+
# element('h1') returns Hpricot::Elem with first h1-tag, or nil if
|
18
|
+
# none exist.
|
19
|
+
def element(selector)
|
20
|
+
@doc.at(selector)
|
21
|
+
end
|
22
|
+
|
23
|
+
# tags('h1') returns the inner HTML of all matched elements mathed.
|
24
|
+
def tags(selector)
|
25
|
+
e = elements(selector)
|
26
|
+
e.map {|x| x.inner_html}
|
27
|
+
end
|
28
|
+
|
29
|
+
# tag('h1') returns the inner HTML of the first mached element, or
|
30
|
+
# nil if none matched.
|
31
|
+
def tag(selector)
|
32
|
+
e = element(selector)
|
33
|
+
e && e.inner_html
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "integrity/project/notifiers"
|
2
|
+
require "integrity/project/push"
|
3
|
+
|
4
|
+
module Integrity
|
5
|
+
class Project
|
6
|
+
include DataMapper::Resource
|
7
|
+
include Notifiers, Push
|
8
|
+
|
9
|
+
property :id, Integer, :serial => true
|
10
|
+
property :name, String, :nullable => false
|
11
|
+
property :permalink, String
|
12
|
+
property :uri, URI, :nullable => false, :length => 255
|
13
|
+
property :branch, String, :nullable => false, :default => "master"
|
14
|
+
property :command, String, :nullable => false, :length => 255, :default => "rake"
|
15
|
+
property :public, Boolean, :default => true
|
16
|
+
property :building, Boolean, :default => false
|
17
|
+
property :created_at, DateTime
|
18
|
+
property :updated_at, DateTime
|
19
|
+
|
20
|
+
has n, :commits, :class_name => "Integrity::Commit"
|
21
|
+
has n, :notifiers, :class_name => "Integrity::Notifier"
|
22
|
+
|
23
|
+
before :save, :set_permalink
|
24
|
+
before :destroy, :delete_working_directory
|
25
|
+
|
26
|
+
validates_is_unique :name
|
27
|
+
|
28
|
+
def build(commit_identifier="HEAD")
|
29
|
+
commit_identifier = head_of_remote_repo if commit_identifier == "HEAD"
|
30
|
+
commit = find_or_create_commit_with_identifier(commit_identifier)
|
31
|
+
|
32
|
+
Build.queue(commit)
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_commit
|
36
|
+
commits.first(:project_id => id, :order => [:committed_at.desc])
|
37
|
+
end
|
38
|
+
|
39
|
+
def previous_commits
|
40
|
+
commits.all(:project_id => id, :order => [:committed_at.desc]).
|
41
|
+
tap {|commits| commits.shift }
|
42
|
+
end
|
43
|
+
|
44
|
+
def status
|
45
|
+
last_commit && last_commit.status
|
46
|
+
end
|
47
|
+
|
48
|
+
def human_readable_status
|
49
|
+
last_commit && last_commit.human_readable_status
|
50
|
+
end
|
51
|
+
|
52
|
+
def public=(flag)
|
53
|
+
attribute_set(:public, case flag
|
54
|
+
when "1", "0" then flag == "1"
|
55
|
+
else !!flag
|
56
|
+
end)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def find_or_create_commit_with_identifier(identifier)
|
61
|
+
# We abuse +committed_at+ here setting it to Time.now because we use it
|
62
|
+
# to sort (for last_commit and previous_commits). I don't like this
|
63
|
+
# very much, but for now it's the only solution I can find.
|
64
|
+
#
|
65
|
+
# This also creates a dependency, as now we *always* have to update the
|
66
|
+
# +committed_at+ field after building to ensure the date is correct :(
|
67
|
+
#
|
68
|
+
# This might also make your commit listings a little jumpy, if some
|
69
|
+
# commits change place every time a build finishes =\
|
70
|
+
commits.first_or_create({:identifier => identifier, :project_id => id},
|
71
|
+
:committed_at => Time.now)
|
72
|
+
end
|
73
|
+
|
74
|
+
def head_of_remote_repo
|
75
|
+
SCM.new(uri, branch).head
|
76
|
+
end
|
77
|
+
|
78
|
+
def set_permalink
|
79
|
+
attribute_set(:permalink, (name || "").downcase.
|
80
|
+
gsub(/'s/, "s").
|
81
|
+
gsub(/&/, "and").
|
82
|
+
gsub(/[^a-z0-9]+/, "-").
|
83
|
+
gsub(/-*$/, ""))
|
84
|
+
end
|
85
|
+
|
86
|
+
def delete_working_directory
|
87
|
+
commits.destroy!
|
88
|
+
ProjectBuilder.delete_working_directory(self)
|
89
|
+
rescue SCM::SCMUnknownError => error
|
90
|
+
Integrity.log "Problem while trying to deleting code: #{error}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Integrity
|
2
|
+
class Project
|
3
|
+
module Notifiers
|
4
|
+
def notifies?(notifier)
|
5
|
+
return false unless notifier = notifiers.first(:name => notifier)
|
6
|
+
|
7
|
+
notifier.enabled?
|
8
|
+
end
|
9
|
+
|
10
|
+
def enabled_notifiers
|
11
|
+
notifiers.all(:enabled => true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def config_for(notifier)
|
15
|
+
notifier = notifiers.first(:name => notifier)
|
16
|
+
notifier ? notifier.config : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_notifiers(to_enable, config)
|
20
|
+
config.each_pair { |name, config|
|
21
|
+
notifier = notifiers.first(:name => name)
|
22
|
+
notifier ||= notifiers.new(:name => name)
|
23
|
+
|
24
|
+
notifier.enabled = to_enable.include?(name)
|
25
|
+
notifier.config = config
|
26
|
+
notifier.save
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|