integrity-bob 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/LICENSE +22 -0
- data/README.rdoc +30 -0
- data/Rakefile +44 -0
- data/bob.gemspec +51 -0
- data/lib/bob/buildable.rb +70 -0
- data/lib/bob/builder.rb +64 -0
- data/lib/bob/engine/foreground.rb +6 -0
- data/lib/bob/engine/threaded.rb +138 -0
- data/lib/bob/engine.rb +9 -0
- data/lib/bob/scm/abstract.rb +59 -0
- data/lib/bob/scm/git.rb +45 -0
- data/lib/bob/scm/svn.rb +45 -0
- data/lib/bob/scm.rb +25 -0
- data/lib/bob.rb +57 -0
- data/lib/core_ext/object.rb +7 -0
- data/test/bob_test.rb +22 -0
- data/test/engine/threaded_test.rb +36 -0
- data/test/helper.rb +30 -0
- data/test/scm/git_test.rb +85 -0
- data/test/scm/svn_test.rb +87 -0
- metadata +133 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2008-2009 Nicolas Sanguinetti, entp.com
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
= Bob the Builder
|
2
|
+
|
3
|
+
Given a Buildable object with a determined API (described in it's documentation),
|
4
|
+
Bob will, when called like:
|
5
|
+
|
6
|
+
Bob.build(buildable, commit_id) # or Bob.build(buildable, [commit_id, commit_id, ...])
|
7
|
+
|
8
|
+
or from your buildable (if you are using the Bob::Buildable mixin provided) as:
|
9
|
+
|
10
|
+
buildable.build(commit_id) # or buildable.build([commit_id, commit_id, ...])
|
11
|
+
|
12
|
+
1. Checkout the buildable on the specified commit
|
13
|
+
2. Call <tt>Buildable#start_building</tt>
|
14
|
+
3. Run the script provided in <tt>Buildable#build_script</tt> in the buildable.
|
15
|
+
4. When the build process finishes, it will call <tt>Buildable#finish_building</tt> with
|
16
|
+
the commit_id, the build status (true if the script returns a status code
|
17
|
+
of 0, false otherwise), and a string with the build output (both STDOUT and STDERR).
|
18
|
+
|
19
|
+
If you pass an array of commits, the steps 1-4 will be repeated for each commit provided,
|
20
|
+
in order.
|
21
|
+
|
22
|
+
== Do I need this?
|
23
|
+
|
24
|
+
Probably not. Check out integrity[http://integrityapp.com] for a full fledged
|
25
|
+
automated CI server, which is what most people need.
|
26
|
+
|
27
|
+
== Credits
|
28
|
+
|
29
|
+
Authors:: Nicolas Sanguinetti (foca[http://github.com/foca]) and Simon Rozet (sr[http://github.com/sr])
|
30
|
+
License:: MIT (Check LICENSE for details)
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "rake/testtask"
|
2
|
+
|
3
|
+
rdoc_sources = %w(hanna/rdoctask rdoc/task rake/rdoctask)
|
4
|
+
begin
|
5
|
+
require rdoc_sources.shift
|
6
|
+
rescue LoadError
|
7
|
+
retry
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require "metric_fu" if RUBY_VERSION < "1.9"
|
12
|
+
rescue LoadError
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
require "mg"
|
17
|
+
MG.new("bob.gemspec")
|
18
|
+
rescue LoadError
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Default: run all tests"
|
22
|
+
task :default => :test
|
23
|
+
|
24
|
+
SCMs = %w[git svn]
|
25
|
+
|
26
|
+
desc "Run unit tests"
|
27
|
+
task :test => SCMs.map { |scm| "test:#{scm}" } do
|
28
|
+
ruby "test/bob_test.rb"
|
29
|
+
ruby "test/engine/threaded_test.rb"
|
30
|
+
end
|
31
|
+
|
32
|
+
SCMs.each { |scm|
|
33
|
+
desc "Run unit tests with #{scm}"
|
34
|
+
task "test:#{scm}" do
|
35
|
+
ruby "test/scm/#{scm}_test.rb"
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
(defined?(RDoc::Task) ? RDoc::Task : Rake::RDocTask).new do |rd|
|
40
|
+
rd.main = "README.rdoc"
|
41
|
+
rd.title = "Documentation for Bob the Builder"
|
42
|
+
rd.rdoc_files.include("README.rdoc", "LICENSE", "lib/**/*.rb")
|
43
|
+
rd.rdoc_dir = "doc"
|
44
|
+
end
|
data/bob.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "bob"
|
3
|
+
s.version = "0.2.0"
|
4
|
+
s.date = "2009-07-02"
|
5
|
+
|
6
|
+
s.description = "Bob the Builder will build your code. Simple."
|
7
|
+
s.summary = "Bob builds!"
|
8
|
+
s.homepage = "http://integrityapp.com"
|
9
|
+
|
10
|
+
s.authors = ["Nicolás Sanguinetti", "Simon Rozet"]
|
11
|
+
s.email = "info@integrityapp.com"
|
12
|
+
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.rubyforge_project = "integrity"
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.rubygems_version = "1.3.1"
|
17
|
+
|
18
|
+
s.add_dependency "addressable"
|
19
|
+
|
20
|
+
if s.respond_to?(:add_development_dependency)
|
21
|
+
s.add_development_dependency "sr-mg"
|
22
|
+
s.add_development_dependency "sr-bob-test"
|
23
|
+
s.add_development_dependency "contest"
|
24
|
+
s.add_development_dependency "redgreen"
|
25
|
+
s.add_development_dependency "ruby-debug"
|
26
|
+
end
|
27
|
+
|
28
|
+
s.files = %w[
|
29
|
+
.gitignore
|
30
|
+
LICENSE
|
31
|
+
README.rdoc
|
32
|
+
Rakefile
|
33
|
+
bob.gemspec
|
34
|
+
lib/bob.rb
|
35
|
+
lib/bob/buildable.rb
|
36
|
+
lib/bob/builder.rb
|
37
|
+
lib/bob/engine.rb
|
38
|
+
lib/bob/engine/foreground.rb
|
39
|
+
lib/bob/engine/threaded.rb
|
40
|
+
lib/bob/scm.rb
|
41
|
+
lib/bob/scm/abstract.rb
|
42
|
+
lib/bob/scm/git.rb
|
43
|
+
lib/bob/scm/svn.rb
|
44
|
+
lib/core_ext/object.rb
|
45
|
+
test/bob_test.rb
|
46
|
+
test/engine/threaded_test.rb
|
47
|
+
test/helper.rb
|
48
|
+
test/scm/git_test.rb
|
49
|
+
test/scm/svn_test.rb
|
50
|
+
]
|
51
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Bob
|
2
|
+
# Mixin to add to your classes.
|
3
|
+
module Buildable
|
4
|
+
# Builds the list of commits you pass. You must provide an
|
5
|
+
# enumerable with commit identifiers appropriate to the
|
6
|
+
# repository the code is in (SHAs if git, rev numbers if svn,
|
7
|
+
# etc), or a single string with a commit id.
|
8
|
+
def build(commits)
|
9
|
+
Bob.build(self, commits)
|
10
|
+
end
|
11
|
+
|
12
|
+
# What kind of repository this buildable represents. Must
|
13
|
+
# return a Symbol (:git, :svn, etc.)
|
14
|
+
#
|
15
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
16
|
+
def scm
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
# Full URI to the repository to clone/checkout.
|
21
|
+
#
|
22
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
23
|
+
def uri
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Branch of the code you want to watch in order to build.
|
28
|
+
#
|
29
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
30
|
+
def branch
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Script that will be run in the buildable's checked out code,
|
35
|
+
# if it returns a status code of 0 it will be considered a
|
36
|
+
# successfull build. Else it will be considered a failed build.
|
37
|
+
#
|
38
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
39
|
+
def build_script
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
# Callback sent when a build starts. The first argument is a
|
44
|
+
# string with whatever identifier is appropriate for a repository
|
45
|
+
# of this kind. The second is a hash with information about the
|
46
|
+
# commit.
|
47
|
+
#
|
48
|
+
# <tt>:author</tt>:: A string with the name/email of the committer
|
49
|
+
# <tt>:message</tt>:: The commit message
|
50
|
+
# <tt>:committed_at</tt>:: A Time object with the timestamp of the
|
51
|
+
# commit
|
52
|
+
#
|
53
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
54
|
+
def start_building(commit_id, commit_info)
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
# Callback sent after a build finishes. The first argument is a
|
59
|
+
# string with whatever identifier is appropriate for a respository
|
60
|
+
# of this kind. The second is a boolean which is true if the build
|
61
|
+
# was successful or false if it failed. And the last one is a string
|
62
|
+
# with the full output returned by the build process (STDOUT and
|
63
|
+
# STDERR interleaved)
|
64
|
+
#
|
65
|
+
# <b>You must implement this in the classes where you mixin this module</b>
|
66
|
+
def finish_building(commit_id, build_status, build_output)
|
67
|
+
raise NotImplementedError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/bob/builder.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Bob
|
2
|
+
# A Builder will take care of building a buildable (wow, you didn't see that coming,
|
3
|
+
# right?).
|
4
|
+
class Builder
|
5
|
+
attr_reader :buildable
|
6
|
+
|
7
|
+
# Instantiate the Builder, passing an object that understands the <tt>Buildable</tt>
|
8
|
+
# interface, and a <tt>commit_id</tt>.
|
9
|
+
#
|
10
|
+
# You can pass <tt>:head</tt> as the commit id, in which case it will resolve to the
|
11
|
+
# head commit of the current branch (for example, "HEAD" under git, or the latest
|
12
|
+
# revision under svn)
|
13
|
+
def initialize(buildable, commit_id)
|
14
|
+
@buildable = buildable
|
15
|
+
@commit_id = commit_id
|
16
|
+
end
|
17
|
+
|
18
|
+
# This is where the magic happens:
|
19
|
+
#
|
20
|
+
# 1. Check out the repo to the appropriate commit.
|
21
|
+
# 2. Notify the buildable that the build is starting.
|
22
|
+
# 3. Run the build script on it in the background.
|
23
|
+
# 4. Reports the build back to the buildable.
|
24
|
+
def build
|
25
|
+
Bob.logger.info "Building #{commit_id} of the #{buildable.scm} repo at #{buildable.uri}"
|
26
|
+
|
27
|
+
in_background do
|
28
|
+
scm.with_commit(commit_id) {
|
29
|
+
buildable.start_building(commit_id, scm.info(commit_id))
|
30
|
+
build_status, build_output = run_build_script
|
31
|
+
buildable.finish_building(commit_id, build_status, build_output)
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def commit_id
|
39
|
+
@commit_id == :head ? scm.head : @commit_id
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_build_script
|
43
|
+
build_output = nil
|
44
|
+
|
45
|
+
Bob.logger.debug("Running the build script for #{buildable.uri}")
|
46
|
+
IO.popen(build_script, "r") { |output| build_output = output.read }
|
47
|
+
Bob.logger.debug("Ran build script `#{build_script}` and got:\n#{build_output}")
|
48
|
+
|
49
|
+
[$?.success?, build_output]
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_script
|
53
|
+
"(cd #{scm.working_dir} && #{buildable.build_script} 2>&1)"
|
54
|
+
end
|
55
|
+
|
56
|
+
def scm
|
57
|
+
@scm ||= SCM.new(buildable.scm, buildable.uri, buildable.branch)
|
58
|
+
end
|
59
|
+
|
60
|
+
def in_background(&block)
|
61
|
+
Bob.engine.call(block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require "thread"
|
2
|
+
|
3
|
+
module Bob
|
4
|
+
module Engine
|
5
|
+
# A thread pool based build engine. This engine simply adds jobs to an
|
6
|
+
# in-memory queue, and processes them as soon as possible.
|
7
|
+
class Threaded
|
8
|
+
# The optional pool size controls how many threads will be created.
|
9
|
+
def initialize(pool_size = 2)
|
10
|
+
@pool = ThreadPool.new(pool_size)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Adds a job to the queue.
|
14
|
+
def call(job)
|
15
|
+
@pool << job
|
16
|
+
end
|
17
|
+
|
18
|
+
# The number of jobs currently in the queue.
|
19
|
+
def njobs
|
20
|
+
@pool.njobs
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method will not return until #njobs returns 0.
|
24
|
+
def wait!
|
25
|
+
Thread.pass until @pool.njobs == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Manage a pool of threads, allowing for spin up / spin down of the
|
29
|
+
# contained threads.
|
30
|
+
# Simply processes work added to it's queue via #push.
|
31
|
+
# The default size for the pool is 2 threads.
|
32
|
+
class ThreadPool
|
33
|
+
# A thread safe single value for use as a counter.
|
34
|
+
class Incrementor
|
35
|
+
# The value passed in will be the initial value.
|
36
|
+
def initialize(v = 0)
|
37
|
+
@m = Mutex.new
|
38
|
+
@v = v
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add the given value to self, default 1.
|
42
|
+
def inc(v = 1)
|
43
|
+
sync { @v += v }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Subtract the given value to self, default 1.
|
47
|
+
def dec(v = 1)
|
48
|
+
sync { @v -= v }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Simply shows the value inspect for convenience.
|
52
|
+
def inspect
|
53
|
+
@v.inspect
|
54
|
+
end
|
55
|
+
|
56
|
+
# Extract the value.
|
57
|
+
def to_i
|
58
|
+
@v
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Wrap the given block in a mutex.
|
64
|
+
def sync(&b)
|
65
|
+
@m.synchronize &b
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# The number of threads in the pool.
|
70
|
+
attr_reader :size
|
71
|
+
|
72
|
+
# The job queue.
|
73
|
+
attr_reader :jobs
|
74
|
+
|
75
|
+
# Set the size of the thread pool. Asynchronously run down threads
|
76
|
+
# that are no longer required, and synchronously spawn new required
|
77
|
+
# threads.
|
78
|
+
def size=(other)
|
79
|
+
@size = other
|
80
|
+
|
81
|
+
if @workers.size > @size
|
82
|
+
(@workers.size - @size).times do
|
83
|
+
@workers.shift[:run] = false
|
84
|
+
end
|
85
|
+
else
|
86
|
+
(@size - @workers.size).times do
|
87
|
+
@workers << spawn
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Default pool size is 2 threads.
|
93
|
+
def initialize(size = nil)
|
94
|
+
size ||= 2
|
95
|
+
@jobs = Queue.new
|
96
|
+
@njobs = Incrementor.new
|
97
|
+
@workers = Array.new(size) { spawn }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Adds a job to the queue, the job can be any number of objects
|
101
|
+
# responding to call, and/or a block.
|
102
|
+
def add(*jobs, &blk)
|
103
|
+
jobs = jobs + Array(blk)
|
104
|
+
|
105
|
+
jobs.each do |job|
|
106
|
+
@jobs << job
|
107
|
+
@njobs.inc
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
alias_method :push, :add
|
112
|
+
alias_method :<<, :add
|
113
|
+
|
114
|
+
# A peak at the number of jobs in the queue. N.B. May differ, but
|
115
|
+
# should be more accurate than +jobs.size+.
|
116
|
+
def njobs
|
117
|
+
@njobs.to_i
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# Create a new thread and return it. The thread will run until the
|
123
|
+
# thread-local value +:run+ is changed to false or nil.
|
124
|
+
def spawn
|
125
|
+
Thread.new do
|
126
|
+
c = Thread.current
|
127
|
+
c[:run] = true
|
128
|
+
|
129
|
+
while c[:run]
|
130
|
+
@jobs.pop.call
|
131
|
+
@njobs.dec
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/bob/engine.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
module Bob
|
2
|
+
# Different engines to run code in background. An engine is any object
|
3
|
+
# that responds to #call and takes a Proc object, which should be executed
|
4
|
+
# "in the background". The different engines are:
|
5
|
+
module Engine
|
6
|
+
autoload :Foreground, "bob/engine/foreground"
|
7
|
+
autoload :Threaded, "bob/engine/threaded"
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Bob
|
2
|
+
module SCM
|
3
|
+
class Abstract
|
4
|
+
attr_reader :uri, :branch
|
5
|
+
|
6
|
+
def initialize(uri, branch)
|
7
|
+
@uri = Addressable::URI.parse(uri)
|
8
|
+
@branch = branch
|
9
|
+
end
|
10
|
+
|
11
|
+
# Checkout the code into <tt>working_dir</tt> at the specified revision
|
12
|
+
# and call the passed block
|
13
|
+
def with_commit(commit_id)
|
14
|
+
update_code
|
15
|
+
checkout(commit_id)
|
16
|
+
yield
|
17
|
+
end
|
18
|
+
|
19
|
+
# Directory where the code will be checked out. Make sure the user
|
20
|
+
# running Bob is allowed to write to this directory (or you'll get a
|
21
|
+
# <tt>Errno::EACCESS</tt>)
|
22
|
+
def working_dir
|
23
|
+
@working_dir ||= "#{Bob.directory}/#{path}".tap { |dir|
|
24
|
+
FileUtils.mkdir_p(dir)
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get some information about the specified commit. Returns a hash with:
|
29
|
+
#
|
30
|
+
# [<tt>:author</tt>] Commit author's name and email
|
31
|
+
# [<tt>:message</tt>] Commit message
|
32
|
+
# [<tt>:committed_at</tt>] Commit date (as a <tt>Time</tt> object)
|
33
|
+
def info(commit_id)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return the identifier for the last commit in this branch of the
|
38
|
+
# repository.
|
39
|
+
def head
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def run(command, cd=true)
|
46
|
+
command = "(#{cd ? "cd #{working_dir} && " : ""}#{command} &>/dev/null)"
|
47
|
+
Bob.logger.debug(command)
|
48
|
+
system(command) || raise(Error, "Couldn't run SCM command `#{command}`")
|
49
|
+
end
|
50
|
+
|
51
|
+
def path
|
52
|
+
@path ||= "#{uri}-#{branch}".
|
53
|
+
gsub(/[^\w_ \-]+/i, '-'). # Remove unwanted chars.
|
54
|
+
gsub(/[ \-]+/i, '-'). # No more than one of the separator in a row.
|
55
|
+
gsub(/^\-|\-$/i, '') # Remove leading/trailing separator.
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/bob/scm/git.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Bob
|
2
|
+
module SCM
|
3
|
+
class Git < Abstract
|
4
|
+
def info(commit_id)
|
5
|
+
format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:committed_at: %ci%n)
|
6
|
+
YAML.load(`cd #{working_dir} && git show -s --pretty=format:"#{format}" #{commit_id}`).tap { |info|
|
7
|
+
info[:committed_at] = Time.parse(info[:committed_at])
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def head
|
12
|
+
`git ls-remote --heads #{uri} #{branch} | cut -f1`.chomp
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def update_code
|
18
|
+
cloned? ? fetch : clone
|
19
|
+
end
|
20
|
+
|
21
|
+
def cloned?
|
22
|
+
File.directory?("#{working_dir}/.git")
|
23
|
+
end
|
24
|
+
|
25
|
+
def clone
|
26
|
+
run "git clone #{uri} #{working_dir}", false
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch
|
30
|
+
git "fetch origin"
|
31
|
+
end
|
32
|
+
|
33
|
+
def checkout(commit_id)
|
34
|
+
# First checkout the branch just in case the commit_id
|
35
|
+
# turns out to be HEAD or other non-sha identifier
|
36
|
+
git "checkout origin/#{branch}"
|
37
|
+
git "reset --hard #{commit_id}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def git(command)
|
41
|
+
run "git #{command}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/bob/scm/svn.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "bob/scm/abstract"
|
2
|
+
|
3
|
+
module Bob
|
4
|
+
module SCM
|
5
|
+
class Svn < Abstract
|
6
|
+
def info(revision)
|
7
|
+
dump = `svn log --non-interactive --revision #{revision} #{uri}`.split("\n")
|
8
|
+
meta = dump[1].split(" | ")
|
9
|
+
|
10
|
+
{ :message => dump[3],
|
11
|
+
:author => meta[1],
|
12
|
+
:committed_at => Time.parse(meta[2]) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def head
|
16
|
+
`svn info #{uri}`.split("\n").detect { |l| l =~ /^Revision: (\d+)/ }
|
17
|
+
$1.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_commit(commit_id)
|
21
|
+
update_code
|
22
|
+
checkout(commit_id)
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_code
|
29
|
+
initial_checkout unless checked_out?
|
30
|
+
end
|
31
|
+
|
32
|
+
def checkout(revision)
|
33
|
+
run("svn up -q -r#{revision}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def initial_checkout(revision=nil)
|
37
|
+
run("svn co -q #{uri} #{working_dir}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def checked_out?
|
41
|
+
File.directory?(working_dir + "/.svn")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/bob/scm.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "bob/scm/abstract"
|
2
|
+
|
3
|
+
module Bob
|
4
|
+
module SCM
|
5
|
+
autoload :Git, "bob/scm/git"
|
6
|
+
autoload :Svn, "bob/scm/svn"
|
7
|
+
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
# Factory to return appropriate SCM instances (according to repository kind)
|
11
|
+
def self.new(scm, uri, branch)
|
12
|
+
class_for(scm).new(uri, branch)
|
13
|
+
end
|
14
|
+
|
15
|
+
# A copy of Inflector.camelize, from ActiveSupport. It will convert
|
16
|
+
# string to UpperCamelCase.
|
17
|
+
def self.class_for(string)
|
18
|
+
class_name = string.to_s.
|
19
|
+
gsub(/\/(.?)/) { "::#{$1.upcase}" }.
|
20
|
+
gsub(/(?:^|_)(.)/) { $1.upcase }
|
21
|
+
const_get(class_name)
|
22
|
+
end
|
23
|
+
private_class_method :class_for
|
24
|
+
end
|
25
|
+
end
|
data/lib/bob.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "yaml"
|
3
|
+
require "logger"
|
4
|
+
require "time"
|
5
|
+
require "addressable/uri"
|
6
|
+
|
7
|
+
require "bob/buildable"
|
8
|
+
require "bob/builder"
|
9
|
+
require "bob/scm"
|
10
|
+
require "bob/engine"
|
11
|
+
require "core_ext/object"
|
12
|
+
|
13
|
+
module Bob
|
14
|
+
# Builds the specified <tt>buildable</tt>. This object must understand
|
15
|
+
# the API described in the README.
|
16
|
+
#
|
17
|
+
# The second argument will take an array of commit_ids, which should be
|
18
|
+
# strings with the relevant identifier (a SHA1 hash for git repositories,
|
19
|
+
# a numerical revision for svn repositories, etc).
|
20
|
+
#
|
21
|
+
# You can pass :head as a commit identifier to build the latest commit
|
22
|
+
# in the repo. Examples:
|
23
|
+
#
|
24
|
+
# Bob.build(buildable, :head) # just build the head
|
25
|
+
# Bob.build(buildable, ["4", "3", "2"]) # build revision 4, 3, and 2
|
26
|
+
# # (in that order)
|
27
|
+
# Bob.build(buildable, [:head, "a30fb12"]) # build the HEAD and a30fb12
|
28
|
+
# # commits in this repo.
|
29
|
+
def self.build(buildable, commit_ids)
|
30
|
+
Array(commit_ids).each do |commit_id|
|
31
|
+
Builder.new(buildable, commit_id).build
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Directory where the code for the different buildables will be checked out.
|
36
|
+
# Make sure the user running Bob is allowed to write to this directory.
|
37
|
+
def self.directory
|
38
|
+
@directory || "/tmp"
|
39
|
+
end
|
40
|
+
|
41
|
+
# What will you use to build in background. Must respond to <tt>call</tt> and
|
42
|
+
# take a block which will be run "in background". The default is to run in
|
43
|
+
# foreground.
|
44
|
+
def self.engine
|
45
|
+
@engine || Engine::Foreground
|
46
|
+
end
|
47
|
+
|
48
|
+
# What to log with (must implement ruby's Logger interface). Logs to STDOUT
|
49
|
+
# by default.
|
50
|
+
def self.logger
|
51
|
+
@logger || Logger.new(STDOUT)
|
52
|
+
end
|
53
|
+
|
54
|
+
class << self
|
55
|
+
attr_writer :directory, :engine, :logger
|
56
|
+
end
|
57
|
+
end
|
data/test/bob_test.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/helper"
|
2
|
+
|
3
|
+
class BobTest < Test::Unit::TestCase
|
4
|
+
test "directory" do
|
5
|
+
Bob.directory = "/foo/bar"
|
6
|
+
assert_equal "/foo/bar", Bob.directory
|
7
|
+
end
|
8
|
+
|
9
|
+
test "logger" do
|
10
|
+
logger = Logger.new("/tmp/bob.log")
|
11
|
+
Bob.logger = logger
|
12
|
+
|
13
|
+
assert_same logger, Bob.logger
|
14
|
+
end
|
15
|
+
|
16
|
+
test "engine" do
|
17
|
+
engine = Object.new
|
18
|
+
Bob.engine = engine
|
19
|
+
|
20
|
+
assert_same engine, Bob.engine
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../helper"
|
2
|
+
|
3
|
+
class ThreadedBobTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
|
7
|
+
@repo = GitRepo.new(:test_repo)
|
8
|
+
@repo.create
|
9
|
+
|
10
|
+
@buildable = BuildableStub.call(@repo)
|
11
|
+
end
|
12
|
+
|
13
|
+
test "with a successful threaded build" do
|
14
|
+
old_engine = Bob.engine
|
15
|
+
|
16
|
+
repo.add_successful_commit
|
17
|
+
commit_id = repo.commits.last[:identifier]
|
18
|
+
|
19
|
+
begin
|
20
|
+
Thread.abort_on_exception = true
|
21
|
+
Bob.engine = Bob::Engine::Threaded.new(5)
|
22
|
+
Bob.build(buildable, commit_id)
|
23
|
+
Bob.engine.wait!
|
24
|
+
|
25
|
+
status, output = buildable.builds[commit_id]
|
26
|
+
assert_equal :successful, status
|
27
|
+
assert_equal "Running tests...\n", output
|
28
|
+
|
29
|
+
commit = buildable.metadata[commit_id]
|
30
|
+
assert_equal "This commit will work", commit[:message]
|
31
|
+
assert_equal Time.now.min, commit[:committed_at].min
|
32
|
+
ensure
|
33
|
+
Bob.engine = old_engine
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "contest"
|
3
|
+
require "hpricot"
|
4
|
+
|
5
|
+
begin
|
6
|
+
require "redgreen"
|
7
|
+
require "ruby-debug"
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"),
|
12
|
+
File.expand_path(File.dirname(__FILE__) + "/../test/helper"))
|
13
|
+
|
14
|
+
require "bob"
|
15
|
+
require "bob/test"
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
include Bob
|
19
|
+
include Bob::Test
|
20
|
+
|
21
|
+
attr_reader :repo, :buildable
|
22
|
+
|
23
|
+
def setup
|
24
|
+
Bob.logger = Logger.new("/dev/null")
|
25
|
+
Bob.engine = Bob::Engine::Foreground
|
26
|
+
Bob.directory = File.expand_path(File.dirname(__FILE__) + "/../tmp")
|
27
|
+
|
28
|
+
FileUtils.rm_rf(Bob.directory) if File.directory?(Bob.directory)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../helper"
|
2
|
+
|
3
|
+
class BobGitTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
|
7
|
+
@repo = GitRepo.new(:test_repo)
|
8
|
+
@repo.create
|
9
|
+
|
10
|
+
@buildable = BuildableStub.call(@repo)
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(uri, branch="master")
|
14
|
+
SCM::Git.new(uri, branch).__send__(:path)
|
15
|
+
end
|
16
|
+
|
17
|
+
test "converts repo uri into a path" do
|
18
|
+
assert_equal "git-github-com-integrity-bob-master",
|
19
|
+
path("git://github.com/integrity/bob")
|
20
|
+
assert_equal "git-example-org-foo-repo-master",
|
21
|
+
path("git@example.org:~foo/repo")
|
22
|
+
assert_equal "tmp-repo-git-master", path("/tmp/repo.git")
|
23
|
+
assert_equal "tmp-repo-git-foo", path("/tmp/repo.git", "foo")
|
24
|
+
end
|
25
|
+
|
26
|
+
test "with a successful build" do
|
27
|
+
repo.add_successful_commit
|
28
|
+
|
29
|
+
commit_id = repo.commits.last[:identifier]
|
30
|
+
|
31
|
+
buildable.build(commit_id)
|
32
|
+
|
33
|
+
status, output = buildable.builds[commit_id]
|
34
|
+
assert_equal :successful, status
|
35
|
+
assert_equal "Running tests...\n", output
|
36
|
+
|
37
|
+
assert_equal 1, buildable.metadata.length
|
38
|
+
|
39
|
+
commit = buildable.metadata[commit_id]
|
40
|
+
assert_equal "This commit will work", commit[:message]
|
41
|
+
assert commit[:committed_at].is_a?(Time)
|
42
|
+
end
|
43
|
+
|
44
|
+
test "with a failed build" do
|
45
|
+
repo.add_failing_commit
|
46
|
+
|
47
|
+
commit_id = repo.commits.last[:identifier]
|
48
|
+
|
49
|
+
buildable.build(commit_id)
|
50
|
+
|
51
|
+
status, output = buildable.builds[commit_id]
|
52
|
+
assert_equal :failed, status
|
53
|
+
assert_equal "Running tests...\n", output
|
54
|
+
|
55
|
+
assert_equal 1, buildable.metadata.length
|
56
|
+
|
57
|
+
commit = buildable.metadata[commit_id]
|
58
|
+
assert_equal "This commit will fail", commit[:message]
|
59
|
+
assert commit[:committed_at].is_a?(Time)
|
60
|
+
end
|
61
|
+
|
62
|
+
test "with multiple commits" do
|
63
|
+
2.times { repo.add_failing_commit }
|
64
|
+
commits = repo.commits.collect { |c| c[:identifier] }
|
65
|
+
|
66
|
+
buildable.build(commits)
|
67
|
+
|
68
|
+
assert_equal 2, commits.length
|
69
|
+
assert_equal 2, buildable.metadata.length
|
70
|
+
assert_equal 2, buildable.builds.length
|
71
|
+
end
|
72
|
+
|
73
|
+
test "can build the head of a repository" do
|
74
|
+
repo.add_failing_commit
|
75
|
+
repo.add_successful_commit
|
76
|
+
|
77
|
+
buildable.build(:head)
|
78
|
+
|
79
|
+
assert_equal 1, buildable.builds.length
|
80
|
+
|
81
|
+
status, output = buildable.builds[repo.head]
|
82
|
+
assert_equal :successful, status
|
83
|
+
assert_equal "Running tests...\n", output
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../helper"
|
2
|
+
|
3
|
+
class BobSvnTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
|
7
|
+
@repo = SvnRepo.new(:test_repo)
|
8
|
+
@repo.create
|
9
|
+
|
10
|
+
@buildable = BuildableStub.call(@repo)
|
11
|
+
end
|
12
|
+
|
13
|
+
def path(uri)
|
14
|
+
SCM::Svn.new(uri, "").__send__(:path)
|
15
|
+
end
|
16
|
+
|
17
|
+
test "converts svn repo uri into a path" do
|
18
|
+
assert_equal "http-rubygems-rubyforge-org-svn",
|
19
|
+
path("http://rubygems.rubyforge.org/svn/")
|
20
|
+
|
21
|
+
assert_equal "svn-rubyforge-org-var-svn-rubygems",
|
22
|
+
path("svn://rubyforge.org/var/svn/rubygems")
|
23
|
+
|
24
|
+
assert_equal "svn-ssh-developername-rubyforge-org-var-svn-rubygems",
|
25
|
+
path("svn+ssh://developername@rubyforge.org/var/svn/rubygems")
|
26
|
+
|
27
|
+
assert_equal "home-user-code-repo",
|
28
|
+
path("/home/user/code/repo")
|
29
|
+
end
|
30
|
+
|
31
|
+
test "with a successful build" do
|
32
|
+
repo.add_successful_commit
|
33
|
+
|
34
|
+
buildable.build("2")
|
35
|
+
|
36
|
+
assert_equal 1, buildable.metadata.length
|
37
|
+
|
38
|
+
status, output = buildable.builds["2"]
|
39
|
+
assert_equal :successful, status
|
40
|
+
assert_equal "Running tests...\n", output
|
41
|
+
|
42
|
+
assert_equal 1, buildable.metadata.length
|
43
|
+
|
44
|
+
commit = buildable.metadata["2"]
|
45
|
+
assert commit[:committed_at].is_a?(Time)
|
46
|
+
assert_equal "This commit will work", commit[:message]
|
47
|
+
end
|
48
|
+
|
49
|
+
test "with a failed build" do
|
50
|
+
repo.add_failing_commit
|
51
|
+
|
52
|
+
buildable.build("2")
|
53
|
+
|
54
|
+
status, output = buildable.builds["2"]
|
55
|
+
assert_equal :failed, status
|
56
|
+
assert_equal "Running tests...\n", output
|
57
|
+
|
58
|
+
assert_equal 1, buildable.metadata.length
|
59
|
+
|
60
|
+
commit = buildable.metadata["2"]
|
61
|
+
assert commit[:committed_at].is_a?(Time)
|
62
|
+
assert_equal "This commit will fail", commit[:message]
|
63
|
+
end
|
64
|
+
|
65
|
+
test "with multiple commits" do
|
66
|
+
repo.add_successful_commit
|
67
|
+
2.times { repo.add_failing_commit }
|
68
|
+
|
69
|
+
buildable.build(repo.commits.collect { |c| c[:identifier] })
|
70
|
+
|
71
|
+
assert_equal 3, buildable.metadata.length
|
72
|
+
assert_equal 3, buildable.builds.length
|
73
|
+
end
|
74
|
+
|
75
|
+
test "can build the head of a repository" do
|
76
|
+
repo.add_failing_commit
|
77
|
+
repo.add_successful_commit
|
78
|
+
|
79
|
+
buildable.build(:head)
|
80
|
+
|
81
|
+
assert_equal 1, buildable.builds.length
|
82
|
+
|
83
|
+
status, output = buildable.builds["3"]
|
84
|
+
assert_equal :successful, status
|
85
|
+
assert_equal "Running tests...\n", output
|
86
|
+
end
|
87
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: integrity-bob
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Nicol\xC3\xA1s Sanguinetti"
|
8
|
+
- Simon Rozet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-07-02 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: addressable
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
version:
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sr-mg
|
28
|
+
type: :development
|
29
|
+
version_requirement:
|
30
|
+
version_requirements: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
version:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sr-bob-test
|
38
|
+
type: :development
|
39
|
+
version_requirement:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: contest
|
48
|
+
type: :development
|
49
|
+
version_requirement:
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: redgreen
|
58
|
+
type: :development
|
59
|
+
version_requirement:
|
60
|
+
version_requirements: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: ruby-debug
|
68
|
+
type: :development
|
69
|
+
version_requirement:
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
version:
|
76
|
+
description: Bob the Builder will build your code. Simple.
|
77
|
+
email: info@integrityapp.com
|
78
|
+
executables: []
|
79
|
+
|
80
|
+
extensions: []
|
81
|
+
|
82
|
+
extra_rdoc_files: []
|
83
|
+
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- LICENSE
|
87
|
+
- README.rdoc
|
88
|
+
- Rakefile
|
89
|
+
- bob.gemspec
|
90
|
+
- lib/bob.rb
|
91
|
+
- lib/bob/buildable.rb
|
92
|
+
- lib/bob/builder.rb
|
93
|
+
- lib/bob/engine.rb
|
94
|
+
- lib/bob/engine/foreground.rb
|
95
|
+
- lib/bob/engine/threaded.rb
|
96
|
+
- lib/bob/scm.rb
|
97
|
+
- lib/bob/scm/abstract.rb
|
98
|
+
- lib/bob/scm/git.rb
|
99
|
+
- lib/bob/scm/svn.rb
|
100
|
+
- lib/core_ext/object.rb
|
101
|
+
- test/bob_test.rb
|
102
|
+
- test/engine/threaded_test.rb
|
103
|
+
- test/helper.rb
|
104
|
+
- test/scm/git_test.rb
|
105
|
+
- test/scm/svn_test.rb
|
106
|
+
has_rdoc: true
|
107
|
+
homepage: http://integrityapp.com
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: "0"
|
118
|
+
version:
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: "0"
|
124
|
+
version:
|
125
|
+
requirements: []
|
126
|
+
|
127
|
+
rubyforge_project: integrity
|
128
|
+
rubygems_version: 1.2.0
|
129
|
+
signing_key:
|
130
|
+
specification_version: 2
|
131
|
+
summary: Bob builds!
|
132
|
+
test_files: []
|
133
|
+
|