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.
Files changed (57) hide show
  1. data/README.markdown +66 -0
  2. data/Rakefile +110 -0
  3. data/VERSION.yml +4 -0
  4. data/app.rb +137 -0
  5. data/bin/integrity +4 -0
  6. data/config/config.sample.ru +31 -0
  7. data/config/config.sample.yml +38 -0
  8. data/config/thin.sample.yml +13 -0
  9. data/integrity.gemspec +76 -0
  10. data/lib/integrity.rb +82 -0
  11. data/lib/integrity/build.rb +61 -0
  12. data/lib/integrity/core_ext/object.rb +6 -0
  13. data/lib/integrity/core_ext/string.rb +5 -0
  14. data/lib/integrity/helpers.rb +16 -0
  15. data/lib/integrity/helpers/authorization.rb +33 -0
  16. data/lib/integrity/helpers/breadcrumbs.rb +20 -0
  17. data/lib/integrity/helpers/forms.rb +28 -0
  18. data/lib/integrity/helpers/pretty_output.rb +45 -0
  19. data/lib/integrity/helpers/rendering.rb +14 -0
  20. data/lib/integrity/helpers/resources.rb +13 -0
  21. data/lib/integrity/helpers/urls.rb +47 -0
  22. data/lib/integrity/installer.rb +132 -0
  23. data/lib/integrity/migrations.rb +152 -0
  24. data/lib/integrity/notifier.rb +50 -0
  25. data/lib/integrity/notifier/base.rb +55 -0
  26. data/lib/integrity/project.rb +117 -0
  27. data/lib/integrity/project_builder.rb +47 -0
  28. data/lib/integrity/scm.rb +19 -0
  29. data/lib/integrity/scm/git.rb +91 -0
  30. data/lib/integrity/scm/git/uri.rb +57 -0
  31. data/public/buttons.css +82 -0
  32. data/public/reset.css +7 -0
  33. data/public/spinner.gif +0 -0
  34. data/test/helpers.rb +48 -0
  35. data/test/helpers/acceptance.rb +126 -0
  36. data/test/helpers/acceptance/git_helper.rb +99 -0
  37. data/test/helpers/acceptance/textfile_notifier.rb +26 -0
  38. data/test/helpers/expectations.rb +5 -0
  39. data/test/helpers/expectations/be_a.rb +23 -0
  40. data/test/helpers/expectations/change.rb +90 -0
  41. data/test/helpers/expectations/have.rb +105 -0
  42. data/test/helpers/expectations/have_tag.rb +128 -0
  43. data/test/helpers/expectations/predicates.rb +37 -0
  44. data/test/helpers/fixtures.rb +83 -0
  45. data/views/_build_info.haml +18 -0
  46. data/views/build.haml +2 -0
  47. data/views/error.haml +36 -0
  48. data/views/home.haml +23 -0
  49. data/views/integrity.sass +387 -0
  50. data/views/layout.haml +28 -0
  51. data/views/new.haml +51 -0
  52. data/views/not_found.haml +31 -0
  53. data/views/notifier.haml +7 -0
  54. data/views/project.builder +21 -0
  55. data/views/project.haml +28 -0
  56. data/views/unauthorized.haml +38 -0
  57. 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,6 @@
1
+ class Object
2
+ def tap
3
+ yield self
4
+ self
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def /(other)
3
+ File.join(self, other)
4
+ end
5
+ 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