brycethornton-integrity 0.1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +66 -0
- data/Rakefile +110 -0
- data/VERSION.yml +4 -0
- data/app.rb +137 -0
- data/bin/integrity +4 -0
- data/config/config.sample.ru +31 -0
- data/config/config.sample.yml +38 -0
- data/config/thin.sample.yml +13 -0
- data/integrity.gemspec +76 -0
- data/lib/integrity.rb +82 -0
- data/lib/integrity/build.rb +61 -0
- data/lib/integrity/core_ext/object.rb +6 -0
- data/lib/integrity/core_ext/string.rb +5 -0
- data/lib/integrity/helpers.rb +16 -0
- data/lib/integrity/helpers/authorization.rb +33 -0
- data/lib/integrity/helpers/breadcrumbs.rb +20 -0
- data/lib/integrity/helpers/forms.rb +28 -0
- data/lib/integrity/helpers/pretty_output.rb +45 -0
- data/lib/integrity/helpers/rendering.rb +14 -0
- data/lib/integrity/helpers/resources.rb +13 -0
- data/lib/integrity/helpers/urls.rb +47 -0
- data/lib/integrity/installer.rb +132 -0
- data/lib/integrity/migrations.rb +152 -0
- data/lib/integrity/notifier.rb +50 -0
- data/lib/integrity/notifier/base.rb +55 -0
- data/lib/integrity/project.rb +117 -0
- data/lib/integrity/project_builder.rb +47 -0
- data/lib/integrity/scm.rb +19 -0
- data/lib/integrity/scm/git.rb +91 -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/helpers.rb +48 -0
- data/test/helpers/acceptance.rb +126 -0
- data/test/helpers/acceptance/git_helper.rb +99 -0
- data/test/helpers/acceptance/textfile_notifier.rb +26 -0
- data/test/helpers/expectations.rb +5 -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/have_tag.rb +128 -0
- data/test/helpers/expectations/predicates.rb +37 -0
- data/test/helpers/fixtures.rb +83 -0
- data/views/_build_info.haml +18 -0
- data/views/build.haml +2 -0
- data/views/error.haml +36 -0
- data/views/home.haml +23 -0
- data/views/integrity.sass +387 -0
- data/views/layout.haml +28 -0
- data/views/new.haml +51 -0
- data/views/not_found.haml +31 -0
- data/views/notifier.haml +7 -0
- data/views/project.builder +21 -0
- data/views/project.haml +28 -0
- data/views/unauthorized.haml +38 -0
- metadata +243 -0
data/lib/integrity.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
__DIR__ = File.dirname(__FILE__)
|
2
|
+
$:.unshift "#{__DIR__}/integrity", *Dir["#{__DIR__}/../vendor/**/lib"].to_a
|
3
|
+
|
4
|
+
require "rubygems"
|
5
|
+
require "json"
|
6
|
+
require "dm-core"
|
7
|
+
require "dm-validations"
|
8
|
+
require "dm-types"
|
9
|
+
require "dm-timestamps"
|
10
|
+
require "dm-aggregates"
|
11
|
+
|
12
|
+
require "yaml"
|
13
|
+
require "logger"
|
14
|
+
require "digest/sha1"
|
15
|
+
require "timeout"
|
16
|
+
require "ostruct"
|
17
|
+
require "fileutils"
|
18
|
+
|
19
|
+
require "core_ext/object"
|
20
|
+
require "core_ext/string"
|
21
|
+
|
22
|
+
require "project"
|
23
|
+
require "build"
|
24
|
+
require "project_builder"
|
25
|
+
require "scm"
|
26
|
+
require "scm/git"
|
27
|
+
require "notifier"
|
28
|
+
|
29
|
+
module Integrity
|
30
|
+
def self.new(config_file = nil)
|
31
|
+
self.config = config_file unless config_file.nil?
|
32
|
+
DataMapper.logger = self.logger if config[:log_debug_info]
|
33
|
+
DataMapper.setup(:default, config[:database_uri])
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.root
|
37
|
+
File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.default_configuration
|
41
|
+
@defaults ||= { :database_uri => "sqlite3::memory:",
|
42
|
+
:export_directory => root / "exports",
|
43
|
+
:log => STDOUT,
|
44
|
+
:base_uri => "http://localhost:8910",
|
45
|
+
:use_basic_auth => false,
|
46
|
+
:build_all_commits => true,
|
47
|
+
:log_debug_info => false }
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.config
|
51
|
+
@config ||= default_configuration
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.config=(file)
|
55
|
+
@config = default_configuration.merge(YAML.load_file(file))
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.log(message, &block)
|
59
|
+
logger.info(message, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.logger
|
63
|
+
@logger ||= Logger.new(config[:log], "daily").tap do |logger|
|
64
|
+
logger.formatter = LogFormatter.new
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.version
|
69
|
+
@version ||= begin
|
70
|
+
file = YAML.load_file(Integrity.root / "VERSION.yml")
|
71
|
+
"#{file['major']}.#{file['minor']}.#{file['patch']}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
class LogFormatter < Logger::Formatter
|
78
|
+
def call(severity, time, progname, msg)
|
79
|
+
time.strftime("[%H:%M:%S] ") + msg2str(msg) + "\n"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Integrity
|
2
|
+
class Build
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial
|
6
|
+
property :output, Text, :nullable => false, :default => ""
|
7
|
+
property :successful, Boolean, :nullable => false, :default => false
|
8
|
+
property :commit_identifier, String, :nullable => false
|
9
|
+
property :commit_metadata, Yaml, :nullable => false, :lazy => false
|
10
|
+
property :created_at, DateTime
|
11
|
+
property :updated_at, DateTime
|
12
|
+
|
13
|
+
belongs_to :project, :class_name => "Integrity::Project"
|
14
|
+
|
15
|
+
def failed?
|
16
|
+
!successful?
|
17
|
+
end
|
18
|
+
|
19
|
+
def status
|
20
|
+
successful? ? :success : :failed
|
21
|
+
end
|
22
|
+
|
23
|
+
def human_readable_status
|
24
|
+
successful? ? "Build Successful" : "Build Failed"
|
25
|
+
end
|
26
|
+
|
27
|
+
def short_commit_identifier
|
28
|
+
sha1?(commit_identifier) ? commit_identifier[0..6] : commit_identifier
|
29
|
+
end
|
30
|
+
|
31
|
+
def commit_metadata
|
32
|
+
case data = attribute_get(:commit_metadata)
|
33
|
+
when String; YAML.load(data)
|
34
|
+
else data
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def commit_author
|
39
|
+
@author ||= begin
|
40
|
+
commit_metadata[:author] =~ /^(.*) <(.*)>$/
|
41
|
+
OpenStruct.new(:name => $1.strip, :email => $2.strip, :full => commit_metadata[:author])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def commit_message
|
46
|
+
commit_metadata[:message]
|
47
|
+
end
|
48
|
+
|
49
|
+
def commited_at
|
50
|
+
case commit_metadata[:date]
|
51
|
+
when String then Time.parse(commit_metadata[:date])
|
52
|
+
else commit_metadata[:date]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def sha1?(string)
|
58
|
+
string =~ /^[a-f0-9]{40}$/
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/helpers/*.rb"].each &method(:require)
|
2
|
+
|
3
|
+
module Integrity
|
4
|
+
module Helpers
|
5
|
+
include Authorization
|
6
|
+
include Breadcrumbs
|
7
|
+
include Forms
|
8
|
+
include PrettyOutput
|
9
|
+
include Rendering
|
10
|
+
include Resources
|
11
|
+
include Urls
|
12
|
+
|
13
|
+
include Rack::Utils
|
14
|
+
alias :h :escape_html
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "diddies"
|
2
|
+
|
3
|
+
module Integrity
|
4
|
+
module Helpers
|
5
|
+
module Authorization
|
6
|
+
include Sinatra::Authorization
|
7
|
+
|
8
|
+
def authorization_realm
|
9
|
+
"Integrity"
|
10
|
+
end
|
11
|
+
|
12
|
+
def authorized?
|
13
|
+
return true unless Integrity.config[:use_basic_auth]
|
14
|
+
!!request.env["REMOTE_USER"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def authorize(user, password)
|
18
|
+
if Integrity.config[:hash_admin_password]
|
19
|
+
password = Digest::SHA1.hexdigest(password)
|
20
|
+
end
|
21
|
+
|
22
|
+
!Integrity.config[:use_basic_auth] ||
|
23
|
+
(Integrity.config[:admin_username] == user &&
|
24
|
+
Integrity.config[:admin_password] == password)
|
25
|
+
end
|
26
|
+
|
27
|
+
def unauthorized!(realm=authorization_realm)
|
28
|
+
response["WWW-Authenticate"] = %(Basic realm="#{realm}")
|
29
|
+
throw :halt, [401, show(:unauthorized, :title => "incorrect credentials")]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module Breadcrumbs
|
4
|
+
def pages
|
5
|
+
@pages ||= [["projects", "/"], ["new project", "/new"]]
|
6
|
+
end
|
7
|
+
|
8
|
+
def breadcrumbs(*crumbs)
|
9
|
+
crumbs[0..-2].map do |crumb|
|
10
|
+
if page_data = pages.detect {|c| c.first == crumb }
|
11
|
+
%Q(<a href="#{page_data.last}">#{page_data.first}</a>)
|
12
|
+
elsif @project && @project.permalink == crumb
|
13
|
+
%Q(<a href="#{project_url(@project)}">#{@project.permalink}</a>)
|
14
|
+
end
|
15
|
+
end + [crumbs.last]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module Forms
|
4
|
+
def errors_on(object, field)
|
5
|
+
return "" unless errors = object.errors.on(field)
|
6
|
+
errors.map {|e| e.gsub(/#{field} /i, "") }.join(", ")
|
7
|
+
end
|
8
|
+
|
9
|
+
def error_class(object, field)
|
10
|
+
object.errors.on(field).nil? ? "" : "with_errors"
|
11
|
+
end
|
12
|
+
|
13
|
+
def checkbox(name, condition, extras={})
|
14
|
+
attrs = { :name => name, :type => "checkbox", :value => "1" }
|
15
|
+
attrs.merge!(:checked => condition ? true : nil)
|
16
|
+
attrs.merge(extras)
|
17
|
+
end
|
18
|
+
|
19
|
+
def notifier_form(notifier)
|
20
|
+
haml(notifier.to_haml, :layout => :notifier, :locals => {
|
21
|
+
:config => current_project.config_for(notifier),
|
22
|
+
:notifier => "#{notifier.to_s.split(/::/).last}",
|
23
|
+
:enabled => current_project.notifies?(notifier)
|
24
|
+
})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module PrettyOutput
|
4
|
+
def cycle(*values)
|
5
|
+
@cycles ||= {}
|
6
|
+
@cycles[values] ||= -1 # first value returned is 0
|
7
|
+
next_value = @cycles[values] = (@cycles[values] + 1) % values.size
|
8
|
+
values[next_value]
|
9
|
+
end
|
10
|
+
|
11
|
+
def bash_color_codes(string)
|
12
|
+
string.gsub("\e[0m", '</span>').
|
13
|
+
gsub("\e[31m", '<span class="color31">').
|
14
|
+
gsub("\e[32m", '<span class="color32">').
|
15
|
+
gsub("\e[33m", '<span class="color33">').
|
16
|
+
gsub("\e[34m", '<span class="color34">').
|
17
|
+
gsub("\e[35m", '<span class="color35">').
|
18
|
+
gsub("\e[36m", '<span class="color36">').
|
19
|
+
gsub("\e[37m", '<span class="color37">')
|
20
|
+
end
|
21
|
+
|
22
|
+
def pretty_date(date_time)
|
23
|
+
days_away = (Date.today - Date.new(date_time.year, date_time.month, date_time.day)).to_i
|
24
|
+
if days_away == 0
|
25
|
+
"today"
|
26
|
+
elsif days_away == 1
|
27
|
+
"yesterday"
|
28
|
+
else
|
29
|
+
strftime_with_ordinal(date_time, "on %b %d%o")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def strftime_with_ordinal(date_time, format_string)
|
34
|
+
ordinal = case date_time.day
|
35
|
+
when 1, 21, 31 then "st"
|
36
|
+
when 2, 22 then "nd"
|
37
|
+
when 3, 23 then "rd"
|
38
|
+
else "th"
|
39
|
+
end
|
40
|
+
|
41
|
+
date_time.strftime(format_string.gsub("%o", ordinal))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module Rendering
|
4
|
+
def show(view, options={})
|
5
|
+
@title = breadcrumbs(*options[:title])
|
6
|
+
haml view
|
7
|
+
end
|
8
|
+
|
9
|
+
def partial(template, locals={})
|
10
|
+
haml("_#{template}".to_sym, :locals => locals, :layout => false)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module Resources
|
4
|
+
def current_project
|
5
|
+
@project ||= Project.first(:permalink => params[:project]) or raise Sinatra::NotFound
|
6
|
+
end
|
7
|
+
|
8
|
+
def current_build
|
9
|
+
@build ||= current_project.builds.first(:commit_identifier => params[:build]) or raise Sinatra::NotFound
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Integrity
|
2
|
+
module Helpers
|
3
|
+
module Urls
|
4
|
+
def url(path)
|
5
|
+
url = "#{request.scheme}://#{request.host}"
|
6
|
+
|
7
|
+
if request.scheme == "https" && request.port != 443 ||
|
8
|
+
request.scheme == "http" && request.port != 80
|
9
|
+
url << ":#{request.port}"
|
10
|
+
end
|
11
|
+
|
12
|
+
url << "/" unless path.index("/").zero?
|
13
|
+
url << path
|
14
|
+
end
|
15
|
+
|
16
|
+
def root_url
|
17
|
+
url("/")
|
18
|
+
end
|
19
|
+
|
20
|
+
def project_path(project, *path)
|
21
|
+
"/" << [project.permalink, *path].join("/")
|
22
|
+
end
|
23
|
+
|
24
|
+
def project_url(project, *path)
|
25
|
+
url project_path(project, *path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def push_url_for(project)
|
29
|
+
Addressable::URI.parse(project_url(project, "push")).tap do |url|
|
30
|
+
if Integrity.config[:use_basic_auth]
|
31
|
+
url.user = Integrity.config[:admin_username]
|
32
|
+
url.password = Integrity.config[:hash_admin_password] ?
|
33
|
+
"<password>" : Integrity.config[:admin_password]
|
34
|
+
end
|
35
|
+
end.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_path(build)
|
39
|
+
"/#{build.project.permalink}/builds/#{build.commit_identifier}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_url(build)
|
43
|
+
url build_path(build)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../integrity"
|
2
|
+
require "thor"
|
3
|
+
|
4
|
+
module Integrity
|
5
|
+
class Installer < Thor
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
desc "install [PATH]",
|
9
|
+
"Copy template files to PATH. Next, go there and edit them."
|
10
|
+
def install(path)
|
11
|
+
@root = File.expand_path(path)
|
12
|
+
|
13
|
+
create_dir_structure
|
14
|
+
copy_template_files
|
15
|
+
edit_template_files
|
16
|
+
create_db(root / "config.yml")
|
17
|
+
after_setup_message
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "create_db [CONFIG]",
|
21
|
+
"Checks the `database_uri` in CONFIG and creates and bootstraps a database for integrity"
|
22
|
+
def create_db(config, direction="up")
|
23
|
+
Integrity.new(config)
|
24
|
+
migrate_db(direction)
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "version",
|
28
|
+
"Print the current integrity version"
|
29
|
+
def version
|
30
|
+
puts Integrity.version
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_reader :root
|
35
|
+
|
36
|
+
def migrate_db(direction="up")
|
37
|
+
require 'migrations'
|
38
|
+
|
39
|
+
set_up_migrations unless migrations_already_set_up?
|
40
|
+
add_initial_migration if tables_from_before_migrations_exist?
|
41
|
+
|
42
|
+
case direction.to_s
|
43
|
+
when "up" then Integrity::Migrations.migrate_up!
|
44
|
+
when "down" then Integrity::Migrations.migrate_down!
|
45
|
+
else raise ArgumentError, "DIRECTION must be either up or down"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_dir_structure
|
50
|
+
mkdir_p root
|
51
|
+
mkdir_p root / "builds"
|
52
|
+
mkdir_p root / "log"
|
53
|
+
mkdir_p root / "public" # this one is to play nice with Passenger
|
54
|
+
mkdir_p root / "tmp" # this one is to play nice with Passenger
|
55
|
+
end
|
56
|
+
|
57
|
+
def copy_template_files
|
58
|
+
cp Integrity.root / "config" / "config.sample.ru", root / "config.ru"
|
59
|
+
cp Integrity.root / "config" / "config.sample.yml", root / "config.yml"
|
60
|
+
cp Integrity.root / "config" / "thin.sample.yml", root / "thin.yml"
|
61
|
+
end
|
62
|
+
|
63
|
+
def edit_template_files
|
64
|
+
edit_integrity_configuration
|
65
|
+
edit_thin_configuration
|
66
|
+
end
|
67
|
+
|
68
|
+
def edit_integrity_configuration
|
69
|
+
config = File.read(root / "config.yml")
|
70
|
+
config.gsub! %r(sqlite3:///var/integrity.db), "sqlite3://#{root}/integrity.db"
|
71
|
+
config.gsub! %r(/path/to/scm/exports), "#{root}/builds"
|
72
|
+
config.gsub! %r(/var/log), "#{root}/log"
|
73
|
+
File.open(root / "config.yml", "w") { |f| f.puts config }
|
74
|
+
end
|
75
|
+
|
76
|
+
def edit_thin_configuration
|
77
|
+
config = File.read(root / "thin.yml")
|
78
|
+
config.gsub! %r(/apps/integrity), root
|
79
|
+
File.open(root / "thin.yml", 'w') { |f| f.puts config }
|
80
|
+
end
|
81
|
+
|
82
|
+
def after_setup_message
|
83
|
+
puts
|
84
|
+
puts %Q(Awesome! Integrity was installed successfully!)
|
85
|
+
puts
|
86
|
+
puts %Q(If you want to enable notifiers, install the gems and then require them)
|
87
|
+
puts %Q(in #{root}/config.ru)
|
88
|
+
puts
|
89
|
+
puts %Q(For example:)
|
90
|
+
puts
|
91
|
+
puts %Q( sudo gem install -s http://gems.github.com foca-integrity-email)
|
92
|
+
puts
|
93
|
+
puts %Q(And then in #{root}/config.ru add:)
|
94
|
+
puts
|
95
|
+
puts %Q( require "notifier/email")
|
96
|
+
puts
|
97
|
+
puts %Q(Don't forget to tweak #{root / "config.yml"} to your needs.)
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_up_migrations
|
101
|
+
database_adapter.execute %q(CREATE TABLE "migration_info" ("migration_name" VARCHAR(255));)
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_initial_migration
|
105
|
+
database_adapter.execute %q(INSERT INTO "migration_info" ("migration_name") VALUES ("initial"))
|
106
|
+
end
|
107
|
+
|
108
|
+
def tables_from_before_migrations_exist?
|
109
|
+
table_exists?("integrity_projects") &&
|
110
|
+
table_exists?("integrity_builds") &&
|
111
|
+
table_exists?("integrity_notifiers")
|
112
|
+
end
|
113
|
+
|
114
|
+
def migrations_already_set_up?
|
115
|
+
table_exists?("migration_info")
|
116
|
+
end
|
117
|
+
|
118
|
+
def without_pluralizing_table_names
|
119
|
+
database_adapter.resource_naming_convention = DataMapper::NamingConventions::Resource::Underscored
|
120
|
+
yield
|
121
|
+
database_adapter.resource_naming_convention = DataMapper::NamingConventions::Resource::UnderscoredAndPluralized
|
122
|
+
end
|
123
|
+
|
124
|
+
def table_exists?(table_name)
|
125
|
+
database_adapter.storage_exists?(table_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
def database_adapter
|
129
|
+
DataMapper.repository(:default).adapter
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|