bob 0.1 → 0.4.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.
- 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
|