bob 0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 CantRunCommand < RuntimeError; end
8
+ class Error < StandardError; end
9
9
 
10
- # Factory to return appropriate SCM instances (according to repository kind)
11
- def self.new(kind, uri, branch)
12
- class_for(kind).new(uri, branch)
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
@@ -4,46 +4,60 @@ module Bob
4
4
  attr_reader :uri, :branch
5
5
 
6
6
  def initialize(uri, branch)
7
- @uri = Addressable::URI.parse(uri)
7
+ @uri = Addressable::URI.parse(uri)
8
8
  @branch = branch
9
9
  end
10
10
 
11
- # Checkout the code into <tt>working_dir</tt> at the specified revision and
12
- # call the passed block
13
- def with_commit(commit_id)
14
- update_code
15
- checkout(commit_id)
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. Make sure the user running Bob is
20
- # allowed to write to this directory (or you'll get a <tt>Errno::EACCESS</tt>)
21
- def working_dir
22
- @working_dir ||= "#{Bob.directory}/#{path_from_uri}".tap do |path|
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
- def run(command)
39
- command = "(cd #{working_dir} && #{command} &>/dev/null)"
40
- Bob.logger.debug command
41
- system(command) || raise(CantRunCommand, "Couldn't run SCM command `#{command}`")
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
- def path_from_uri
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(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 do |info|
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
- protected
8
+ dump = YAML.load(`cd #{dir_for(commit)} && git show -s \
9
+ --pretty=format:"#{format}" #{commit}`)
12
10
 
13
- def path_from_uri
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
- private
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
- def cloned?
28
- File.directory?("#{working_dir}/.git")
29
- end
30
-
31
- def clone
32
- git "clone #{uri} #{working_dir}"
33
- rescue CantRunCommand
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
- def git(command)
53
- run "git #{command}"
54
- end
26
+ def cloned?(commit)
27
+ dir_for(commit).join(".git").directory?
28
+ end
55
29
  end
56
30
  end
57
31
  end
@@ -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,3 @@
1
+ require "bob"
2
+ require "bob/test/repo"
3
+ require "bob/test/builder_stub"
@@ -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 File.dirname(__FILE__) + "/helper"
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,3 @@
1
+ contest 0.1.2
2
+ hpricot 0.8.1
3
+ redgreen 1.2.2
@@ -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