integrity 0.1.8
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 +165 -0
- data/VERSION.yml +4 -0
- data/app.rb +138 -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 +80 -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 +157 -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 +83 -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 +47 -0
- data/test/helpers/acceptance.rb +127 -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 +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
|
data/public/buttons.css
ADDED
@@ -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;}
|
data/public/spinner.gif
ADDED
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
|