integrity 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/README.markdown +66 -0
  2. data/Rakefile +165 -0
  3. data/VERSION.yml +4 -0
  4. data/app.rb +138 -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 +80 -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 +157 -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 +83 -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 +47 -0
  35. data/test/helpers/acceptance.rb +127 -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 +258 -0
@@ -0,0 +1,47 @@
1
+ module Integrity
2
+ class ProjectBuilder
3
+ attr_reader :build_script
4
+
5
+ def initialize(project)
6
+ @uri = project.uri
7
+ @build_script = project.command
8
+ @branch = project.branch
9
+ @scm = SCM.new(@uri, @branch, export_directory)
10
+ @build = Build.new(:project => project)
11
+ end
12
+
13
+ def build(commit)
14
+ Integrity.log "Building #{commit} (#{@branch}) of #{@build.project.name} in #{export_directory} using #{scm_name}"
15
+ @scm.with_revision(commit) { run_build_script }
16
+ @build
17
+ ensure
18
+ @build.commit_identifier = @scm.commit_identifier(commit)
19
+ @build.commit_metadata = @scm.commit_metadata(commit)
20
+ @build.save
21
+ end
22
+
23
+ def delete_code
24
+ FileUtils.rm_r export_directory
25
+ rescue Errno::ENOENT
26
+ nil
27
+ end
28
+
29
+ private
30
+ def export_directory
31
+ Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
32
+ end
33
+
34
+ def scm_name
35
+ @scm.name
36
+ end
37
+
38
+ def run_build_script
39
+ Integrity.log "Running `#{build_script}` in #{@scm.working_directory}"
40
+
41
+ IO.popen "(cd #{@scm.working_directory} && #{build_script}) 2>&1", "r" do |pipe|
42
+ @build.output = pipe.read
43
+ end
44
+ @build.successful = $?.success?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ module Integrity
2
+ module SCM
3
+ class SCMUnknownError < StandardError; end
4
+
5
+ def self.new(uri, *args)
6
+ scm_class_for(uri).new(uri, *args)
7
+ end
8
+
9
+ def self.working_tree_path(uri)
10
+ scm_class_for(uri).working_tree_path(uri)
11
+ end
12
+
13
+ private
14
+ def self.scm_class_for(uri)
15
+ return Git if uri.scheme == "git" || uri.path =~ /\.git\/?/
16
+ raise SCMUnknownError, "could not find any SCM based on URI '#{uri.to_s}'"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ module Integrity
2
+ module SCM
3
+ class Git
4
+ require File.dirname(__FILE__) / "git/uri"
5
+
6
+ attr_reader :uri, :branch, :working_directory
7
+
8
+ def self.working_tree_path(uri)
9
+ Git::URI.new(uri).working_tree_path
10
+ end
11
+
12
+ def initialize(uri, branch, working_directory)
13
+ @uri = uri.to_s
14
+ @branch = branch.to_s
15
+ @working_directory = working_directory
16
+ end
17
+
18
+ def with_revision(revision)
19
+ fetch_code
20
+ checkout(revision)
21
+ yield
22
+ end
23
+
24
+ def commit_identifier(sha1)
25
+ `cd #{working_directory} && git show -s --pretty=format:%H #{sha1}`.chomp
26
+ end
27
+
28
+ def commit_metadata(sha1)
29
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:date: %ci%n)
30
+ YAML.load(`cd #{working_directory} && git show -s --pretty=format:"#{format}" #{sha1}`)
31
+ end
32
+
33
+ def name
34
+ self.class.name.split("::").last
35
+ end
36
+
37
+ private
38
+
39
+ def fetch_code
40
+ clone unless cloned?
41
+ checkout unless on_branch?
42
+ pull
43
+ end
44
+
45
+ def clone
46
+ log "Cloning #{uri} to #{working_directory}"
47
+ `git clone #{uri} #{working_directory} &>/dev/null`
48
+ end
49
+
50
+ def checkout(treeish=nil)
51
+ strategy = case
52
+ when treeish then treeish
53
+ when local_branches.include?(branch) then branch
54
+ else "-b #{branch} origin/#{branch}"
55
+ end
56
+
57
+ log "Checking-out #{strategy}"
58
+ `cd #{working_directory} && git checkout #{strategy} &>/dev/null`
59
+ end
60
+
61
+ def pull
62
+ log "Pull-ing in #{working_directory}"
63
+ `cd #{working_directory} && git pull &>/dev/null`
64
+ end
65
+
66
+ def local_branches
67
+ `cd #{working_directory} && git branch`.split("\n").map {|b| b.delete("*").strip }
68
+ end
69
+
70
+ def cloned?
71
+ File.directory?(working_directory / ".git")
72
+ end
73
+
74
+ def on_branch?
75
+ File.basename(`cd #{working_directory} && git symbolic-ref HEAD &>/dev/null`).chomp == branch
76
+ end
77
+
78
+ def log(message)
79
+ Integrity.log("Git") { message }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,57 @@
1
+ module Integrity
2
+ module SCM
3
+ class Git
4
+ # From the git-pull man page:
5
+ #
6
+ # GIT URLS
7
+ # One of the following notations can be used to name the remote repository:
8
+ #
9
+ # rsync://host.xz/path/to/repo.git/
10
+ # http://host.xz/path/to/repo.git/
11
+ # git://host.xz/~user/path/to/repo.git/
12
+ # ssh://[user@]host.xz[:port]/path/to/repo.git/
13
+ # ssh://[user@]host.xz/path/to/repo.git/
14
+ # ssh://[user@]host.xz/~user/path/to/repo.git/
15
+ # ssh://[user@]host.xz/~/path/to/repo.git
16
+ #
17
+ # SSH is the default transport protocol over the network. You can optionally
18
+ # specify which user to log-in as, and an alternate, scp-like syntax is also
19
+ # supported
20
+ #
21
+ # Both syntaxes support username expansion, as does the native git protocol,
22
+ # but only the former supports port specification. The following three are
23
+ # identical to the last three above, respectively:
24
+ #
25
+ # [user@]host.xz:/path/to/repo.git/
26
+ # [user@]host.xz:~user/path/to/repo.git/
27
+ # [user@]host.xz:path/to/repo.git
28
+ #
29
+ class URI
30
+ def initialize(uri_string)
31
+ @uri = Addressable::URI.parse(uri_string)
32
+ end
33
+
34
+ def working_tree_path
35
+ strip_extension(path).gsub("/", "-")
36
+ end
37
+
38
+ private
39
+
40
+ def strip_extension(string)
41
+ uri = Pathname.new(string)
42
+ if uri.extname.any?
43
+ uri = Pathname.new(string)
44
+ string.gsub(Regexp.new("#{uri.extname}\/?"), "")
45
+ else
46
+ string
47
+ end
48
+ end
49
+
50
+ def path
51
+ path = @uri.path
52
+ path.gsub(/\~[a-zA-Z0-9]*\//, "").gsub(/^\//, "")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,82 @@
1
+ /* --------------------------------------------------------------
2
+
3
+ buttons.css
4
+ * Gives you some great CSS-only buttons.
5
+
6
+ Created by Kevin Hale [particletree.com]
7
+ * particletree.com/features/rediscovering-the-button-element
8
+
9
+ See Readme.txt in this folder for instructions.
10
+
11
+ -------------------------------------------------------------- */
12
+
13
+ button {
14
+ display:block;
15
+ float:left;
16
+ margin:0 0.583em 0.667em 0;
17
+ padding:5px 10px 5px 7px; /* Links */
18
+
19
+ border:1px solid #dedede;
20
+ border-top:1px solid #eee;
21
+ border-left:1px solid #eee;
22
+
23
+ background-color:#f5f5f5;
24
+ font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif;
25
+ font-size:100%;
26
+ line-height:130%;
27
+ text-decoration:none;
28
+ font-weight:bold;
29
+ color:#565656;
30
+ cursor:pointer;
31
+ }
32
+ button {
33
+ width:auto;
34
+ overflow:visible;
35
+ padding:4px 10px 3px 7px; /* IE6 */
36
+ }
37
+ button[type] {
38
+ padding:4px 10px 4px 7px; /* Firefox */
39
+ line-height:17px; /* Safari */
40
+ }
41
+ *:first-child+html button[type] {
42
+ padding:4px 10px 3px 7px; /* IE7 */
43
+ }
44
+ button img {
45
+ margin:0 3px -3px 0 !important;
46
+ padding:0;
47
+ border:none;
48
+ width:16px;
49
+ height:16px;
50
+ float:none;
51
+ }
52
+
53
+
54
+ /* Button colors
55
+ -------------------------------------------------------------- */
56
+
57
+ /* Standard */
58
+ button:hover {
59
+ background-color:#dff4ff;
60
+ border:1px solid #c2e1ef;
61
+ color:#336699;
62
+ }
63
+
64
+ /* Positive */
65
+ body .positive {
66
+ color:#529214;
67
+ }
68
+ button.positive:hover {
69
+ background-color:#E6EFC2;
70
+ border:1px solid #C6D880;
71
+ color:#529214;
72
+ }
73
+
74
+ /* Negative */
75
+ body .negative {
76
+ color:#d12f19;
77
+ }
78
+ button.negative:hover {
79
+ background:#fbe3e4;
80
+ border:1px solid #fbc2c4;
81
+ color:#d12f19;
82
+ }
data/public/reset.css ADDED
@@ -0,0 +1,7 @@
1
+ /*
2
+ Copyright (c) 2008, Yahoo! Inc. All rights reserved.
3
+ Code licensed under the BSD License:
4
+ http://developer.yahoo.net/yui/license.txt
5
+ version: 2.5.2
6
+ */
7
+ html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;font-variant:normal;}sup {vertical-align:text-top;}sub {vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}
Binary file
data/test/helpers.rb ADDED
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + "/../lib/integrity"
2
+
3
+ begin
4
+ require "test/unit"
5
+ require "redgreen"
6
+ require "context"
7
+ require "storyteller"
8
+ require "pending"
9
+ require "matchy"
10
+ require "rr"
11
+ require "mocha"
12
+ require "ruby-debug"
13
+ rescue LoadError
14
+ puts "You're missing some gems required to run the tests."
15
+ puts "Please run `rake test:install_dependencies`"
16
+ puts "You'll probably need to run that command as root or with sudo."
17
+ puts
18
+ puts "Thanks :)"
19
+ puts
20
+
21
+ exit 1
22
+ end
23
+
24
+ require File.dirname(__FILE__) / "helpers" / "expectations"
25
+ require File.dirname(__FILE__) / "helpers" / "fixtures"
26
+
27
+ module TestHelper
28
+ def setup_and_reset_database!
29
+ DataMapper.setup(:default, "sqlite3::memory:")
30
+ DataMapper.auto_migrate!
31
+ end
32
+
33
+ def ignore_logs!
34
+ stub(Integrity).log { nil }
35
+ end
36
+ end
37
+
38
+ class Test::Unit::TestCase
39
+ class << self
40
+ alias_method :specify, :test
41
+ end
42
+
43
+ include RR::Adapters::TestUnit
44
+ include Integrity
45
+ include TestHelper
46
+ end
47
+
@@ -0,0 +1,127 @@
1
+ require 'webrat/rack'
2
+ require 'sinatra'
3
+ require 'sinatra/test'
4
+
5
+ set :environment, :test
6
+ disable :run
7
+ disable :reload
8
+
9
+ Webrat.configuration.instance_variable_set("@mode", :sinatra)
10
+
11
+ module Webrat
12
+ class SinatraSession < Session
13
+ DEFAULT_DOMAIN = "integrity.example.org"
14
+
15
+ def initialize(context = nil)
16
+ super(context)
17
+ @sinatra_test = Sinatra::TestHarness.new
18
+ end
19
+
20
+ %w(get head post put delete).each do |verb|
21
+ class_eval <<-METHOD
22
+ def #{verb}(path, data, headers = {})
23
+ params = data.inject({}) do |data, (key,value)|
24
+ data[key] = Rack::Utils.unescape(value)
25
+ data
26
+ end
27
+ headers['HTTP_HOST'] = DEFAULT_DOMAIN
28
+ @sinatra_test.#{verb}(path, params, headers)
29
+ end
30
+ METHOD
31
+ end
32
+
33
+ def response_body
34
+ @sinatra_test.body
35
+ end
36
+
37
+ def response_code
38
+ @sinatra_test.status
39
+ end
40
+
41
+ private
42
+
43
+ def response
44
+ @sinatra_test.response
45
+ end
46
+
47
+ def current_host
48
+ URI.parse(current_url).host || DEFAULT_DOMAIN
49
+ end
50
+
51
+ def response_location_host
52
+ URI.parse(response_location).host || DEFAULT_DOMAIN
53
+ end
54
+ end
55
+ end
56
+
57
+ require Integrity.root / "app"
58
+ require File.dirname(__FILE__) / "acceptance/git_helper"
59
+
60
+ module AcceptanceHelper
61
+ include FileUtils
62
+
63
+ def export_directory
64
+ Integrity.root / "exports"
65
+ end
66
+
67
+ def enable_auth!
68
+ Integrity.config[:use_basic_auth] = true
69
+ Integrity.config[:admin_username] = "admin"
70
+ Integrity.config[:admin_password] = "test"
71
+ Integrity.config[:hash_admin_password] = false
72
+ end
73
+
74
+ def login_as(user, password)
75
+ def AcceptanceHelper.logged_in; true; end
76
+ basic_auth user, password
77
+ visit "/login"
78
+ Sinatra::Application.before { login_required if AcceptanceHelper.logged_in }
79
+ end
80
+
81
+ def log_out
82
+ def AcceptanceHelper.logged_in; false; end
83
+ @_webrat_session = Webrat::SinatraSession.new(self)
84
+ end
85
+
86
+ def disable_auth!
87
+ Integrity.config[:use_basic_auth] = false
88
+ end
89
+
90
+ def set_and_create_export_directory!
91
+ FileUtils.rm_r(export_directory) if File.directory?(export_directory)
92
+ FileUtils.mkdir(export_directory)
93
+ Integrity.config[:export_directory] = export_directory
94
+ end
95
+
96
+ def setup_log!
97
+ pathname = Integrity.root / "integrity.log"
98
+ FileUtils.rm pathname if File.exists?(pathname)
99
+ Integrity.config[:log] = pathname
100
+ end
101
+ end
102
+
103
+ class Test::Unit::AcceptanceTestCase < Test::Unit::TestCase
104
+ include AcceptanceHelper
105
+ include Test::Storyteller
106
+ include GitHelper
107
+ include Webrat::Methods
108
+ Webrat::Methods.delegate_to_session :response_code
109
+
110
+ before(:all) do
111
+ Integrity.config[:base_uri] = "http://#{Webrat::SinatraSession::DEFAULT_DOMAIN}"
112
+ end
113
+
114
+ before(:each) do
115
+ # ensure each scenario is run in a clean sandbox
116
+ setup_and_reset_database!
117
+ enable_auth!
118
+ setup_log!
119
+ set_and_create_export_directory!
120
+ log_out
121
+ end
122
+
123
+ after(:each) do
124
+ destroy_all_git_repos
125
+ rm_r export_directory if File.directory?(export_directory)
126
+ end
127
+ end