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/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