bob 0.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +2 -46
- data/Rakefile +10 -18
- data/bob.gemspec +15 -14
- data/deps.rip +1 -0
- data/lib/bob.rb +13 -15
- data/lib/bob/builder.rb +25 -36
- data/lib/bob/{background_engines.rb → engine.rb} +3 -3
- data/lib/bob/engine/threaded.rb +145 -0
- data/lib/bob/scm.rb +4 -12
- data/lib/bob/scm/abstract.rb +41 -27
- data/lib/bob/scm/git.rb +18 -44
- data/lib/bob/scm/svn.rb +30 -0
- data/lib/bob/test.rb +3 -0
- data/lib/bob/test/builder_stub.rb +34 -0
- data/lib/bob/test/repo.rb +163 -0
- data/test/bob_test.rb +3 -2
- data/test/deps.rip +3 -0
- data/test/engine/threaded_test.rb +52 -0
- data/test/git_test.rb +26 -0
- data/test/helper.rb +10 -13
- data/test/mixin/scm.rb +46 -0
- data/test/svn_test.rb +31 -0
- data/test/test_test.rb +26 -0
- metadata +19 -49
- data/lib/bob/background_engines/foreground.rb +0 -6
- data/lib/core_ext/object.rb +0 -7
- data/test/helper/buildable_stub.rb +0 -55
- data/test/helper/git_helper.rb +0 -48
data/lib/bob/scm.rb
CHANGED
@@ -5,19 +5,11 @@ module Bob
|
|
5
5
|
autoload :Git, "bob/scm/git"
|
6
6
|
autoload :Svn, "bob/scm/svn"
|
7
7
|
|
8
|
-
class
|
8
|
+
class Error < StandardError; end
|
9
9
|
|
10
|
-
# Factory to return appropriate SCM instances
|
11
|
-
def self.new(
|
12
|
-
|
10
|
+
# Factory to return appropriate SCM instances
|
11
|
+
def self.new(scm, uri, branch)
|
12
|
+
const_get(scm.to_s.capitalize).new(uri, branch)
|
13
13
|
end
|
14
|
-
|
15
|
-
# A copy of Inflector.camelize, from ActiveSupport. It will convert
|
16
|
-
# string to UpperCamelCase.
|
17
|
-
def self.class_for(kind)
|
18
|
-
class_name = kind.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
19
|
-
const_get(class_name)
|
20
|
-
end
|
21
|
-
private_class_method :class_for
|
22
14
|
end
|
23
15
|
end
|
data/lib/bob/scm/abstract.rb
CHANGED
@@ -4,46 +4,60 @@ module Bob
|
|
4
4
|
attr_reader :uri, :branch
|
5
5
|
|
6
6
|
def initialize(uri, branch)
|
7
|
-
@uri
|
7
|
+
@uri = Addressable::URI.parse(uri)
|
8
8
|
@branch = branch
|
9
9
|
end
|
10
10
|
|
11
|
-
# Checkout the code
|
12
|
-
#
|
13
|
-
def with_commit(
|
14
|
-
|
15
|
-
checkout(
|
16
|
-
yield
|
11
|
+
# Checkout the code at the specified <tt>commit</tt> and call the
|
12
|
+
# passed block.
|
13
|
+
def with_commit(commit)
|
14
|
+
commit = resolve(commit)
|
15
|
+
checkout(commit)
|
16
|
+
yield(commit)
|
17
17
|
end
|
18
18
|
|
19
|
-
# Directory where the code will be checked out
|
20
|
-
#
|
21
|
-
def
|
22
|
-
|
23
|
-
FileUtils.mkdir_p path
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# Get some information about the specified commit. Returns a hash with:
|
28
|
-
#
|
29
|
-
# [<tt>:author</tt>] Commit author's name and email
|
30
|
-
# [<tt>:message</tt>] Commit message
|
31
|
-
# [<tt>:committed_at</tt>] Commit date (as a <tt>Time</tt> object)
|
32
|
-
def info(commit_id)
|
33
|
-
raise NotImplementedError
|
19
|
+
# Directory where the code will be checked out for the given
|
20
|
+
# <tt>commit</tt>.
|
21
|
+
def dir_for(commit)
|
22
|
+
Bob.directory.join(path, resolve(commit))
|
34
23
|
end
|
35
24
|
|
36
25
|
protected
|
37
26
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
27
|
+
# Get some information about the specified <tt>commit</tt>.
|
28
|
+
# Returns a hash with:
|
29
|
+
#
|
30
|
+
# [<tt>identifier</tt>] Commit identifier
|
31
|
+
# [<tt>author</tt>] Commit author's name and email
|
32
|
+
# [<tt>message</tt>] Commit message
|
33
|
+
# [<tt>committed_at</tt>] Commit date (as a <tt>Time</tt> object)
|
34
|
+
def info(commit)
|
35
|
+
raise NotImplementedError
|
42
36
|
end
|
43
37
|
|
44
|
-
|
38
|
+
# Return the identifier for the last commit in this branch of the
|
39
|
+
# repository.
|
40
|
+
def head
|
45
41
|
raise NotImplementedError
|
46
42
|
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def run(cmd, dir=nil)
|
46
|
+
cmd = "(#{dir ? "cd #{dir} && " : ""}#{cmd} &>/dev/null)"
|
47
|
+
Bob.logger.debug(cmd)
|
48
|
+
system(cmd) || raise(Error, "Couldn't run SCM command `#{cmd}`")
|
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
|
+
|
58
|
+
def resolve(commit)
|
59
|
+
commit == :head ? head : commit
|
60
|
+
end
|
47
61
|
end
|
48
62
|
end
|
49
63
|
end
|
data/lib/bob/scm/git.rb
CHANGED
@@ -1,57 +1,31 @@
|
|
1
1
|
module Bob
|
2
2
|
module SCM
|
3
3
|
class Git < Abstract
|
4
|
-
def info(
|
5
|
-
format
|
6
|
-
|
7
|
-
info[:committed_at] = Time.parse(info[:committed_at])
|
8
|
-
end
|
9
|
-
end
|
4
|
+
def info(commit)
|
5
|
+
format = "---%nidentifier: %H%nauthor: %an " +
|
6
|
+
"<%ae>%nmessage: >-%n %s%ncommitted_at: %ci%n"
|
10
7
|
|
11
|
-
|
8
|
+
dump = YAML.load(`cd #{dir_for(commit)} && git show -s \
|
9
|
+
--pretty=format:"#{format}" #{commit}`)
|
12
10
|
|
13
|
-
|
14
|
-
path = uri.path.
|
15
|
-
gsub(/\~[a-z0-9]*\//i, ""). # remove ~foobar/
|
16
|
-
gsub(/\s+|\.|\//, "-"). # periods, spaces, slashes -> hyphens
|
17
|
-
gsub(/^-+|-+$/, "") # remove trailing hyphens
|
18
|
-
path += "-#{branch}"
|
11
|
+
dump.update("committed_at" => Time.parse(dump["committed_at"]))
|
19
12
|
end
|
20
13
|
|
21
|
-
|
22
|
-
|
23
|
-
def update_code
|
24
|
-
cloned? ? fetch : clone
|
14
|
+
def head
|
15
|
+
`git ls-remote --heads #{uri} #{branch} | cut -f1`.chomp
|
25
16
|
end
|
26
17
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
FileUtils.rm_r working_dir
|
35
|
-
retry
|
36
|
-
end
|
37
|
-
|
38
|
-
def fetch
|
39
|
-
git "fetch origin"
|
40
|
-
end
|
41
|
-
|
42
|
-
def checkout(commit_id)
|
43
|
-
# First checkout the branch just in case the commit_id turns out to be HEAD or other non-sha identifier
|
44
|
-
git "checkout origin/#{branch}"
|
45
|
-
git "reset --hard #{commit_id}"
|
46
|
-
end
|
47
|
-
|
48
|
-
def reset(commit_id)
|
49
|
-
git "reset --hard #{commit_id}"
|
50
|
-
end
|
18
|
+
private
|
19
|
+
def checkout(commit)
|
20
|
+
run "git clone #{uri} #{dir_for(commit)}" unless cloned?(commit)
|
21
|
+
run "git fetch origin", dir_for(commit)
|
22
|
+
run "git checkout origin/#{branch}", dir_for(commit)
|
23
|
+
run "git reset --hard #{commit}", dir_for(commit)
|
24
|
+
end
|
51
25
|
|
52
|
-
|
53
|
-
|
54
|
-
|
26
|
+
def cloned?(commit)
|
27
|
+
dir_for(commit).join(".git").directory?
|
28
|
+
end
|
55
29
|
end
|
56
30
|
end
|
57
31
|
end
|
data/lib/bob/scm/svn.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Bob
|
2
|
+
module SCM
|
3
|
+
class Svn < Abstract
|
4
|
+
def info(rev)
|
5
|
+
dump = `svn log --non-interactive --revision #{rev} #{uri}`.split("\n")
|
6
|
+
meta = dump[1].split(" | ")
|
7
|
+
|
8
|
+
{ "identifier" => rev,
|
9
|
+
"message" => dump[3],
|
10
|
+
"author" => meta[1],
|
11
|
+
"committed_at" => Time.parse(meta[2]) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def head
|
15
|
+
`svn info #{uri}`.split("\n").detect { |l| l =~ /^Revision: (\d+)/ }
|
16
|
+
$1.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def checkout(rev)
|
21
|
+
run "svn co -q #{uri} #{dir_for(rev)}" unless checked_out?(rev)
|
22
|
+
run "svn up -q -r#{rev}", dir_for(rev)
|
23
|
+
end
|
24
|
+
|
25
|
+
def checked_out?(rev)
|
26
|
+
dir_for(rev).join(".svn").directory?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/bob/test.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Bob
|
2
|
+
module Test
|
3
|
+
class BuilderStub < Bob::Builder
|
4
|
+
def self.for(repo, commit)
|
5
|
+
new(
|
6
|
+
"scm" => repo.scm,
|
7
|
+
"uri" => repo.uri,
|
8
|
+
"branch" => repo.branch,
|
9
|
+
"commit" => commit,
|
10
|
+
"command" => repo.command
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :status, :output, :commit_info
|
15
|
+
|
16
|
+
def initialize(buildable)
|
17
|
+
super
|
18
|
+
|
19
|
+
@status = nil
|
20
|
+
@output = ""
|
21
|
+
@commit_info = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def started(commit_info)
|
25
|
+
@commit_info = commit_info
|
26
|
+
end
|
27
|
+
|
28
|
+
def completed(status, output)
|
29
|
+
@status = status ? :successful : :failed
|
30
|
+
@output = output
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Bob::Test
|
2
|
+
class AbstractRepo
|
3
|
+
def initialize(name = "test_repo")
|
4
|
+
@path = Bob.directory.join(name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def create
|
8
|
+
add_commit("First commit") {
|
9
|
+
`echo 'just a test repo' >> README`
|
10
|
+
add "README"
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_commit(message)
|
15
|
+
Dir.chdir(@path) {
|
16
|
+
yield
|
17
|
+
commit(message)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_successful_commit
|
22
|
+
add_commit("This commit will work") {
|
23
|
+
`echo '#{script(0)}' > test`
|
24
|
+
`chmod +x test`
|
25
|
+
add "test"
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_failing_commit
|
30
|
+
add_commit("This commit will fail") {
|
31
|
+
system "echo '#{script(1)}' > test"
|
32
|
+
system "chmod +x test"
|
33
|
+
add "test"
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def head
|
38
|
+
commits.last["identifier"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def short_head
|
42
|
+
head[0..6]
|
43
|
+
end
|
44
|
+
|
45
|
+
def command
|
46
|
+
"./test"
|
47
|
+
end
|
48
|
+
|
49
|
+
def script(status)
|
50
|
+
<<SH
|
51
|
+
#!/bin/sh
|
52
|
+
echo "Running tests..."
|
53
|
+
exit #{status}
|
54
|
+
SH
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class GitRepo < AbstractRepo
|
59
|
+
def scm
|
60
|
+
"git"
|
61
|
+
end
|
62
|
+
|
63
|
+
def branch
|
64
|
+
"master"
|
65
|
+
end
|
66
|
+
|
67
|
+
def uri
|
68
|
+
@path
|
69
|
+
end
|
70
|
+
|
71
|
+
def add(file)
|
72
|
+
`git add #{file}`
|
73
|
+
end
|
74
|
+
|
75
|
+
def commit(message)
|
76
|
+
`git commit -m "#{message}"`
|
77
|
+
end
|
78
|
+
|
79
|
+
def head
|
80
|
+
Dir.chdir(@path) { `git log --pretty=format:%H | head -1`.chomp }
|
81
|
+
end
|
82
|
+
|
83
|
+
def create
|
84
|
+
FileUtils.mkdir(@path)
|
85
|
+
|
86
|
+
Dir.chdir(@path) {
|
87
|
+
`git init`
|
88
|
+
`git config user.name 'John Doe'`
|
89
|
+
`git config user.email 'johndoe@example.org'`
|
90
|
+
}
|
91
|
+
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
def commits
|
96
|
+
Dir.chdir(@path) {
|
97
|
+
`git log --pretty=oneline`.collect { |l| l.split(" ").first }.
|
98
|
+
inject([]) { |acc, sha1|
|
99
|
+
fmt = "---%nmessage: >-%n %s%ntimestamp: %ci%n" +
|
100
|
+
"identifier: %H%nauthor: %n name: %an%n email: %ae%n"
|
101
|
+
acc << YAML.load(`git show -s --pretty=format:"#{fmt}" #{sha1}`)
|
102
|
+
}.reverse
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class SvnRepo < AbstractRepo
|
108
|
+
def initialize(name = "test_repo")
|
109
|
+
super
|
110
|
+
|
111
|
+
server = @path.join("..", "svn-server")
|
112
|
+
server.mkdir
|
113
|
+
@remote = server.join(@path.basename)
|
114
|
+
end
|
115
|
+
|
116
|
+
def uri
|
117
|
+
"file://#{@remote}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def branch
|
121
|
+
""
|
122
|
+
end
|
123
|
+
|
124
|
+
def scm
|
125
|
+
"svn"
|
126
|
+
end
|
127
|
+
|
128
|
+
def commit(msg)
|
129
|
+
`svn commit -m "#{msg}"`
|
130
|
+
`svn up`
|
131
|
+
end
|
132
|
+
|
133
|
+
def add(file)
|
134
|
+
`svn add #{file}`
|
135
|
+
end
|
136
|
+
|
137
|
+
def create
|
138
|
+
`svnadmin create #{@remote}`
|
139
|
+
|
140
|
+
@remote.join("conf", "svnserve.conf").open("w") { |f|
|
141
|
+
f.puts "[general]"
|
142
|
+
f.puts "anon-access = write"
|
143
|
+
f.puts "auth-access = write"
|
144
|
+
}
|
145
|
+
|
146
|
+
`svn checkout file://#{@remote} #{@path}`
|
147
|
+
|
148
|
+
super
|
149
|
+
end
|
150
|
+
|
151
|
+
# TODO get rid of the Hpricot dependency
|
152
|
+
def commits
|
153
|
+
Dir.chdir(@path) do
|
154
|
+
doc = Hpricot::XML(`svn log --xml`)
|
155
|
+
(doc/:log/:logentry).inject([]) { |acc, c|
|
156
|
+
acc << { "identifier" => c["revision"],
|
157
|
+
"message" => c.at("msg").inner_html,
|
158
|
+
"committed_at" => Time.parse(c.at("date").inner_html) }
|
159
|
+
}.reverse
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/test/bob_test.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
require
|
1
|
+
require "helper"
|
2
2
|
|
3
3
|
class BobTest < Test::Unit::TestCase
|
4
4
|
test "directory" do
|
5
5
|
Bob.directory = "/foo/bar"
|
6
|
-
assert_equal "/foo/bar", Bob.directory
|
6
|
+
assert_equal "/foo/bar", Bob.directory.to_s
|
7
|
+
assert_instance_of Pathname, Bob.directory
|
7
8
|
end
|
8
9
|
|
9
10
|
test "logger" do
|
data/test/deps.rip
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
class ThreadedBobTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
|
7
|
+
@repo = GitRepo.new(:test_repo)
|
8
|
+
@repo.create
|
9
|
+
end
|
10
|
+
|
11
|
+
test "with a successful threaded build" do
|
12
|
+
old_engine = Bob.engine
|
13
|
+
|
14
|
+
repo.add_successful_commit
|
15
|
+
commit_id = repo.commits.last["identifier"]
|
16
|
+
buildable = BuildableStub.for(@repo, commit_id)
|
17
|
+
|
18
|
+
begin
|
19
|
+
Thread.abort_on_exception = true
|
20
|
+
Bob.engine = Bob::Engine::Threaded.new(5)
|
21
|
+
buildable.build
|
22
|
+
Bob.engine.wait!
|
23
|
+
|
24
|
+
assert_equal :successful, buildable.status
|
25
|
+
assert_equal "Running tests...\n", buildable.output
|
26
|
+
|
27
|
+
commit = buildable.commit_info
|
28
|
+
assert_equal "This commit will work", commit["message"]
|
29
|
+
assert_equal Time.now.min, commit["committed_at"].min
|
30
|
+
ensure
|
31
|
+
Bob.engine = old_engine
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class FakeLogger
|
36
|
+
attr_reader :msg
|
37
|
+
|
38
|
+
def error(msg)
|
39
|
+
@msg = msg
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
test "when something goes wrong" do
|
44
|
+
logger = FakeLogger.new
|
45
|
+
|
46
|
+
engine = Bob::Engine::Threaded.new(2, logger)
|
47
|
+
engine.call(proc { fail "foo" })
|
48
|
+
engine.wait!
|
49
|
+
|
50
|
+
assert_equal "Exception occured during build: foo", logger.msg
|
51
|
+
end
|
52
|
+
end
|