foca-integrity 0.1.0

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.
@@ -0,0 +1,83 @@
1
+ module Integrity
2
+ class Project
3
+ include DataMapper::Resource
4
+
5
+ property :id, Integer, :serial => true
6
+ property :name, String, :nullable => false
7
+ property :permalink, String
8
+ property :uri, URI, :nullable => false, :length => 255
9
+ property :branch, String, :nullable => false, :default => "master"
10
+ property :command, String, :nullable => false, :length => 255, :default => "rake"
11
+ property :public, Boolean, :default => true
12
+ property :building, Boolean, :default => false
13
+ property :created_at, DateTime
14
+ property :updated_at, DateTime
15
+
16
+ has n, :builds, :class_name => "Integrity::Build"
17
+ has n, :notifiers, :class_name => "Integrity::Notifier"
18
+
19
+ before :save, :set_permalink
20
+ before :destroy, :delete_code
21
+
22
+ validates_is_unique :name
23
+
24
+ def build(commit_identifier="HEAD")
25
+ return if building?
26
+ update_attributes(:building => true)
27
+ Builder.new(self).build(commit_identifier)
28
+ ensure
29
+ update_attributes(:building => false)
30
+ send_notifications
31
+ end
32
+
33
+ def last_build
34
+ builds.last
35
+ end
36
+
37
+ def previous_builds
38
+ return [] if builds.count <= 1
39
+ builds.all(:order => [:created_at.desc], :offset => 1, :limit => builds.count - 1)
40
+ end
41
+
42
+ def status
43
+ last_build && last_build.status
44
+ end
45
+
46
+ def public=(flag)
47
+ attribute_set(:public, !!flag)
48
+ end
49
+
50
+ def config_for(notifier)
51
+ notifier = notifiers.first(:name => notifier.to_s.split(/::/).last)
52
+ notifier.blank? ? {} : notifier.config
53
+ end
54
+
55
+ def notifies?(notifier)
56
+ !notifiers.first(:name => notifier.to_s.split(/::/).last).blank?
57
+ end
58
+
59
+ def enable_notifiers(*args)
60
+ Notifier.enable_notifiers(id, *args)
61
+ end
62
+
63
+ private
64
+ def set_permalink
65
+ self.permalink = (name || "").downcase.
66
+ gsub(/'s/, "s").
67
+ gsub(/&/, "and").
68
+ gsub(/[^a-z0-9]+/, "-").
69
+ gsub(/-*$/, "")
70
+ end
71
+
72
+ def delete_code
73
+ builds.destroy!
74
+ Builder.new(self).delete_code
75
+ end
76
+
77
+ def send_notifications
78
+ notifiers.each do |notifier|
79
+ notifier.notify_of_build last_build
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,22 @@
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
+
15
+ def self.scm_class_for(string)
16
+ case string.to_s
17
+ when /\.git\/?/ then Git
18
+ else raise SCMUnknownError, "could not find any SCM based on string '#{string}'"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,72 @@
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
+ private
34
+
35
+ def fetch_code
36
+ clone unless cloned?
37
+ checkout unless on_branch?
38
+ pull
39
+ end
40
+
41
+ def clone
42
+ `git clone #{uri} #{working_directory}`
43
+ end
44
+
45
+ def checkout(treeish=nil)
46
+ strategy = case
47
+ when treeish then treeish
48
+ when local_branches.include?(branch) then branch
49
+ else "-b #{branch} origin/#{branch}"
50
+ end
51
+
52
+ `cd #{working_directory} && git checkout #{strategy}`
53
+ end
54
+
55
+ def pull
56
+ `cd #{working_directory} && git pull`
57
+ end
58
+
59
+ def local_branches
60
+ `cd #{working_directory} && git branch`.split("\n").map {|b| b.delete("*").strip }
61
+ end
62
+
63
+ def cloned?
64
+ File.directory?(working_directory / ".git")
65
+ end
66
+
67
+ def on_branch?
68
+ File.basename(`cd #{working_directory} && git symbolic-ref HEAD`).chomp == branch
69
+ end
70
+ end
71
+ end
72
+ 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,3 @@
1
+ module Integrity
2
+ VERSION = "0.1.0"
3
+ 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
+ }
@@ -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
@@ -0,0 +1,91 @@
1
+ module FormFieldHpricotMatchers
2
+ # TODO: Add support for selects
3
+ class HaveField
4
+ include RspecHpricotMatchers
5
+
6
+ def initialize(id, type, tagname)
7
+ @tagname = tagname
8
+ @type = type
9
+ @id = id
10
+ @tag_matcher = have_tag("#{@tagname}##{@id}", @tagname == "textarea" ? @value : nil)
11
+ @label_set = true # always check for a label, unless explicitly told not to
12
+ end
13
+
14
+ def named(name)
15
+ @name_set = true
16
+ @name = name
17
+ self
18
+ end
19
+
20
+ def with_label(label)
21
+ @label = label
22
+ self
23
+ end
24
+
25
+ def without_label
26
+ @label_set = false
27
+ self
28
+ end
29
+
30
+ def with_value(value)
31
+ @value_set = true
32
+ @value = value
33
+ self
34
+ end
35
+
36
+ def checked
37
+ @checked = "checked"
38
+ self
39
+ end
40
+
41
+ def unchecked
42
+ @checked = ""
43
+ self
44
+ end
45
+
46
+ def matches?(actual)
47
+ (@label_set ? have_tag("label[@for=#{@id}]", @label).matches?(actual) : true) &&
48
+ @tag_matcher.matches?(actual) do |field|
49
+ field["type"].should == @type if @type
50
+ field["name"].should == @name if @name_set
51
+ field["value"].should == @value if @value_set && @tagname == "input"
52
+ field["checked"].should == @checked if @checked
53
+ end
54
+ end
55
+
56
+ def failure_message
57
+ attrs = [
58
+ "id ##{@id}",
59
+ @name && "name '#{@name}'",
60
+ @type && "type '#{@type}'",
61
+ @label && "labelled '#{@label}'",
62
+ @value && "value '#{@value}'"
63
+ ].compact.join(", ")
64
+ "You expected a #{@tagname}#{@type ? " (#{@type})" : ""} with #{attrs} but found none.\n\n#{@tag_matcher.failure_message}"
65
+ end
66
+ end
67
+
68
+ def have_field(id, type="text", tagname="input")
69
+ HaveField.new(id, type, tagname)
70
+ end
71
+
72
+ def have_textfield(id)
73
+ have_field(id)
74
+ end
75
+
76
+ def have_password(id)
77
+ have_field(id, "password")
78
+ end
79
+
80
+ def have_checkbox(id)
81
+ have_field(id, "checkbox")
82
+ end
83
+
84
+ def have_radio(id)
85
+ have_field(id, "radio")
86
+ end
87
+
88
+ def have_textarea(id)
89
+ have_field(id, nil, "textarea")
90
+ end
91
+ end
@@ -0,0 +1,131 @@
1
+ require File.dirname(__FILE__) + '/../lib/integrity'
2
+ require 'spec'
3
+
4
+ module NotifierSpecHelper
5
+ def self.included(mod)
6
+ mod.before(:each) { Integrity.stub!(:config).and_return(:base_uri => "http://localhost:4567") }
7
+ end
8
+
9
+ class Integrity::Notifier::Stub < Integrity::Notifier::Base
10
+ def self.to_haml
11
+ ""
12
+ end
13
+
14
+ def deliver!
15
+ nil
16
+ end
17
+ end
18
+
19
+ def mock_build(messages={})
20
+ messages = {
21
+ :project => stub("project", :name => "Integrity", :permalink => "integrity"),
22
+ :commit_identifier => "e7e02bc669d07064cdbc7e7508a21a41e040e70d",
23
+ :short_commit_identifier => "e7e02b",
24
+ :status => :success,
25
+ :successful? => true,
26
+ :commit_message => "the commit message",
27
+ :commit_author => stub("author", :name => "Nicolás Sanguinetti"),
28
+ :commited_at => Time.mktime(2008, 07, 25, 18, 44),
29
+ :output => "the output \e[31mwith color coding\e[0m"
30
+ }.merge(messages)
31
+ @build ||= stub("build", messages)
32
+ end
33
+
34
+ def notifier_config(overrides={})
35
+ @config ||= overrides
36
+ end
37
+
38
+ def notifier
39
+ @notifier ||= stub("notifier", :method_missing => nil)
40
+ end
41
+
42
+ def the_form(locals = {})
43
+ locals = { :config => {} }.merge(locals)
44
+ require 'haml'
45
+ @form ||= Haml::Engine.new(klass.to_haml).render(self, locals)
46
+ end
47
+ end
48
+
49
+ describe "A notifier", :shared => true do
50
+ it "should have a `notify_of_build' class method" do
51
+ klass.should respond_to(:notify_of_build)
52
+ end
53
+
54
+ it "should have a `to_haml' class method" do
55
+ klass.should respond_to(:to_haml)
56
+ end
57
+ end
58
+
59
+ module DatabaseSpecHelper
60
+ def self.included(mod)
61
+ mod.before(:each) { setup_database! }
62
+ end
63
+
64
+ def setup_database!
65
+ DataMapper.setup(:default, 'sqlite3::memory:')
66
+ Integrity::Project.auto_migrate!
67
+ Integrity::Build.auto_migrate!
68
+ Integrity::Notifier.auto_migrate!
69
+ end
70
+ end
71
+
72
+ module AppSpecHelper
73
+ def self.included(mod)
74
+ require 'rspec_hpricot_matchers'
75
+ require Integrity.root / 'spec/form_field_matchers'
76
+
77
+ mod.send(:include, DatabaseSpecHelper)
78
+ mod.send(:include, RspecHpricotMatchers)
79
+ mod.send(:include, FormFieldHpricotMatchers)
80
+ end
81
+
82
+ def mock_project(messages={})
83
+ messages = {
84
+ :name => "Integrity",
85
+ :permalink => "integrity",
86
+ :new_record? => false,
87
+ :uri => "git://github.com/foca/integrity.git",
88
+ :branch => "master",
89
+ :command => "rake",
90
+ :public? => true,
91
+ :builds => [],
92
+ :config_for => {},
93
+ :build => nil,
94
+ :update_attributes => true,
95
+ :save => true,
96
+ :destroy => nil,
97
+ :errors => stub("errors", :on => nil),
98
+ :notifies? => false,
99
+ :enable_notifiers => nil
100
+ }.merge(messages)
101
+
102
+ @project ||= stub("project", messages)
103
+ end
104
+
105
+ def mock_build(messages={})
106
+ messages = {
107
+ :status => :success,
108
+ :successful? => true,
109
+ :output => 'output',
110
+ :project => @project,
111
+ :commit_identifier => '9f6302002d2259c05a64767e0dedb15d280a4848',
112
+ :commit_author => mock("author",
113
+ :name => 'Nicolás Sanguinetti',
114
+ :email => 'contacto@nicolassanguinetti.info',
115
+ :full =>'Nicolás Sanguinetti <contacto@nicolassanguinetti.info>'
116
+ ),
117
+ :commited_at => Time.mktime(2008, 7, 24, 17, 15),
118
+ :commit_message => "Add Object#tap for versions of ruby that don't have it"
119
+ }.merge(messages)
120
+ messages[:short_commit_identifier] = messages[:commit_identifier][0..5]
121
+ mock('build', messages)
122
+ end
123
+
124
+ def disable_basic_auth!
125
+ Integrity.stub!(:config).and_return(:use_basic_auth => false)
126
+ end
127
+
128
+ def enable_basic_auth!
129
+ Integrity.stub!(:config).and_return(:use_basic_auth => true, :admin_username => 'user', :admin_password => 'pass')
130
+ end
131
+ end