xcoder 0.1.15 → 0.1.18
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/.gitignore +6 -0
- data/.rbenv-version +1 -0
- data/.rvmrc +1 -1
- data/Gemfile +10 -2
- data/README.md +110 -9
- data/Rakefile +2 -2
- data/bin/xcoder +74 -0
- data/lib/xcode/builder.rb +1 -2
- data/lib/xcode/builder/base_builder.rb +231 -102
- data/lib/xcode/builder/build_parser.rb +146 -0
- data/lib/xcode/builder/project_target_config_builder.rb +2 -2
- data/lib/xcode/builder/scheme_builder.rb +29 -12
- data/lib/xcode/buildspec.rb +286 -0
- data/lib/xcode/configuration_list.rb +24 -24
- data/lib/xcode/deploy/ftp.rb +56 -0
- data/lib/xcode/deploy/kickfolio.rb +18 -0
- data/lib/xcode/deploy/s3.rb +38 -0
- data/lib/xcode/deploy/ssh.rb +43 -0
- data/lib/xcode/deploy/templates/index.rhtml +22 -0
- data/lib/xcode/deploy/templates/manifest.rhtml +31 -0
- data/lib/xcode/deploy/testflight.rb +32 -27
- data/lib/xcode/deploy/web_assets.rb +39 -0
- data/lib/xcode/info_plist.rb +16 -0
- data/lib/xcode/keychain.rb +33 -10
- data/lib/xcode/platform.rb +65 -0
- data/lib/xcode/project.rb +7 -3
- data/lib/xcode/provisioning_profile.rb +38 -2
- data/lib/xcode/scheme.rb +44 -17
- data/lib/xcode/shell/command.rb +79 -5
- data/lib/xcode/terminal_output.rb +116 -0
- data/lib/xcode/test/formatters/junit_formatter.rb +7 -2
- data/lib/xcode/test/formatters/stdout_formatter.rb +34 -25
- data/lib/xcode/test/parsers/kif_parser.rb +87 -0
- data/lib/xcode/test/parsers/ocunit_parser.rb +3 -3
- data/lib/xcode/version.rb +1 -1
- data/lib/xcode/workspace.rb +13 -5
- data/lib/xcoder.rb +33 -31
- data/spec/TestProject/TestProject.xcodeproj/project.pbxproj +1627 -1015
- data/spec/TestWorkspace.xcworkspace/contents.xcworkspacedata +7 -0
- data/spec/builder_spec.rb +87 -71
- data/spec/deploy_spec.rb +63 -0
- data/spec/ocunit_parser_spec.rb +1 -1
- data/xcoder.gemspec +3 -1
- metadata +95 -19
- data/lib/xcode/buildfile.rb +0 -101
- data/lib/xcode/shell.rb +0 -26
- data/spec/deploy_testflight_spec.rb +0 -27
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module Xcode
|
4
|
+
module TerminalOutput
|
5
|
+
@@colour_enabled = true
|
6
|
+
@@log_level = :info
|
7
|
+
|
8
|
+
LEVELS = [
|
9
|
+
:error,
|
10
|
+
:warning,
|
11
|
+
:notice,
|
12
|
+
:info,
|
13
|
+
:debug
|
14
|
+
]
|
15
|
+
|
16
|
+
def log_level
|
17
|
+
@@log_level
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log_level=(level)
|
21
|
+
raise "Unknown log level #{level}, should be one of #{LEVELS.join(', ')}" unless LEVELS.include? level
|
22
|
+
@@log_level = level
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.included(base)
|
26
|
+
@@colour_supported = terminal_supports_colors?
|
27
|
+
end
|
28
|
+
|
29
|
+
def color_output= color_output
|
30
|
+
@@colour_enabled = color_output
|
31
|
+
end
|
32
|
+
|
33
|
+
def color_output?
|
34
|
+
@@colour_supported and @@colour_enabled
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Print an IO input interaction
|
39
|
+
#
|
40
|
+
def print_input message, level=:debug
|
41
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
42
|
+
puts format_lhs("", "", "<") + message, :default
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Print an IO output interaction
|
47
|
+
def print_output message, level=:debug
|
48
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
49
|
+
puts format_lhs("", "", ">") + message, :default
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_system message, level=:debug
|
53
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
54
|
+
puts format_lhs("", "", "!") + message, :green
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_lhs(left, right, terminator=":")
|
58
|
+
# "#{left.to_s.ljust(10)} #{right.rjust(6)}#{terminator} "
|
59
|
+
"#{right.to_s.rjust(7)}#{terminator} "
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_task(task, message, level=:info, cr=true)
|
63
|
+
return if LEVELS.index(level) > LEVELS.index(@@log_level)
|
64
|
+
|
65
|
+
level_str = ""
|
66
|
+
case level
|
67
|
+
when :error
|
68
|
+
level_str = "ERROR"
|
69
|
+
color = :red
|
70
|
+
when :warning
|
71
|
+
level_str = "WARN"
|
72
|
+
color = :yellow
|
73
|
+
when :notice
|
74
|
+
level_str = "NOTICE"
|
75
|
+
color = :green
|
76
|
+
when :info
|
77
|
+
level_str = ""
|
78
|
+
color = :blue
|
79
|
+
else
|
80
|
+
color = :default
|
81
|
+
end
|
82
|
+
|
83
|
+
print format_lhs(task, level_str), color
|
84
|
+
print message, (level==:warning or level==:error or level==:notice) ? color : :default
|
85
|
+
|
86
|
+
if block_given?
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
|
90
|
+
print "\n" if cr
|
91
|
+
end
|
92
|
+
|
93
|
+
def puts(text, color = :default)
|
94
|
+
color_params = color_output? ? color : {}
|
95
|
+
super(text.colorize(color_params))
|
96
|
+
end
|
97
|
+
|
98
|
+
def print(text, color = :default)
|
99
|
+
color_params = color_output? ? color : {}
|
100
|
+
super(text.colorize(color_params))
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.terminal_supports_colors?
|
104
|
+
# No colors unless we are being run via a TTY
|
105
|
+
return false unless $stdout.isatty
|
106
|
+
|
107
|
+
# Check if the terminal supports colors
|
108
|
+
colors = `tput colors 2> /dev/null`.chomp
|
109
|
+
if $?.exitstatus == 0
|
110
|
+
colors.to_i >= 8
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -6,7 +6,7 @@ module Xcode
|
|
6
6
|
module Formatters
|
7
7
|
class JunitFormatter
|
8
8
|
def initialize(dir)
|
9
|
-
@dir = File.expand_path(dir)
|
9
|
+
@dir = File.expand_path(dir).to_s
|
10
10
|
FileUtils.mkdir_p(@dir)
|
11
11
|
end
|
12
12
|
|
@@ -42,12 +42,17 @@ module Xcode
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
File.open(
|
45
|
+
File.open(File.join(@dir, sanitize_filename("TEST-#{report.name}.xml")), 'w') do |current_file|
|
46
46
|
current_file.write xml.target!
|
47
47
|
end
|
48
48
|
|
49
49
|
end # write
|
50
50
|
|
51
|
+
private
|
52
|
+
def sanitize_filename(filename)
|
53
|
+
filename.gsub(/[^0-9A-Za-z.\-]/, '_')
|
54
|
+
end
|
55
|
+
|
51
56
|
end # JUnitFormatter
|
52
57
|
|
53
58
|
end # Formatters
|
@@ -2,44 +2,53 @@ module Xcode
|
|
2
2
|
module Test
|
3
3
|
module Formatters
|
4
4
|
class StdoutFormatter
|
5
|
+
include Xcode::TerminalOutput
|
5
6
|
|
6
|
-
def initialize
|
7
|
+
def initialize(options = {})
|
7
8
|
@errors = []
|
9
|
+
@test_count = 0
|
10
|
+
options.each { |k,v| self.send("#{k}=", v) }
|
8
11
|
end
|
9
12
|
|
10
13
|
def before(report)
|
11
|
-
|
14
|
+
print_task :test, "Begin tests", :info
|
12
15
|
end
|
13
16
|
|
14
17
|
def after(report)
|
15
|
-
|
16
|
-
@errors.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
level = @errors.count>0 ? :error : :info
|
19
|
+
if @errors.count>0
|
20
|
+
print_task :test, "The following failures occured:", :warning
|
21
|
+
@errors.each do |e|
|
22
|
+
print_task :test, "[#{e.suite.name} #{e.name}]", :error
|
23
|
+
e.errors.each do |error|
|
24
|
+
print_task :test, " #{error[:message]}", :error
|
25
|
+
print_task :test, " at #{error[:location]}", :error
|
26
|
+
if error[:data].count>0
|
27
|
+
print_task :test, "\n Test Output:", :error
|
28
|
+
print_task :test, " > #{error[:data].join(" > ")}\n\n", :error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# if there is left over data in the test report, show that
|
33
|
+
if e.data.count>0
|
34
|
+
print_task :test, "\n There was this trailing output after the above failures", :error
|
35
|
+
print_task :test, " > #{e.data.join(" > ")}\n\n", :error
|
24
36
|
end
|
25
|
-
end
|
26
|
-
|
27
|
-
# if there is left over data in the test report, show that
|
28
|
-
if e.data.count>0
|
29
|
-
puts "\n There was this trailing output after the above failures"
|
30
|
-
puts " > #{e.data.join(" > ")}\n\n"
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
|
-
|
40
|
+
print_task :test, "Finished in #{report.duration} seconds", :info
|
41
|
+
print_task :test, "#{@test_count} tests, #{@errors.count} failures", report.failed? ? :error : :info
|
35
42
|
end
|
36
43
|
|
37
44
|
def before_suite(suite)
|
38
|
-
|
45
|
+
print_task :test, "#{suite.name}: ", :info, false
|
39
46
|
end
|
40
47
|
|
41
48
|
def after_suite(suite)
|
42
|
-
|
49
|
+
color = (suite.total_passed_tests == suite.tests.count) ? :info : :error
|
50
|
+
#print_task :test, "#{suite.total_passed_tests}/#{suite.tests.count}", color
|
51
|
+
puts " [#{suite.total_passed_tests}/#{suite.tests.count}]", color
|
43
52
|
end
|
44
53
|
|
45
54
|
def before_test(test)
|
@@ -47,14 +56,14 @@ module Xcode
|
|
47
56
|
end
|
48
57
|
|
49
58
|
def after_test(test)
|
59
|
+
@test_count += 1
|
50
60
|
if test.passed?
|
51
|
-
print "."
|
61
|
+
print ".", :green
|
52
62
|
elsif test.failed?
|
53
|
-
print "F"
|
63
|
+
print "F", :red
|
54
64
|
@errors << test
|
55
|
-
end
|
56
|
-
|
57
|
-
end
|
65
|
+
end
|
66
|
+
end
|
58
67
|
|
59
68
|
end # StdoutFormatter
|
60
69
|
end # Formatters
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'xcode/test/report'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Xcode
|
5
|
+
module Test
|
6
|
+
module Parsers
|
7
|
+
|
8
|
+
class KIFParser
|
9
|
+
attr_accessor :report, :builder
|
10
|
+
|
11
|
+
def initialize(report = Xcode::Test::Report.new)
|
12
|
+
@report = report
|
13
|
+
@awaiting_scenario_name = false
|
14
|
+
yield self if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def flush
|
18
|
+
@report.finish
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(piped_row)
|
22
|
+
if @awaiting_scenario_name
|
23
|
+
if match = piped_row.match(/\[\d+\:.+\]\s(.+)/)
|
24
|
+
name = match[1].strip
|
25
|
+
@report.add_suite name, Time.now
|
26
|
+
@awaiting_scenario_name = false
|
27
|
+
end
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
case piped_row.force_encoding("UTF-8")
|
32
|
+
|
33
|
+
when /BEGIN KIF TEST RUN: (\d+) scenarios/
|
34
|
+
@report.start
|
35
|
+
|
36
|
+
when /BEGIN SCENARIO (\d+)\/(\d+) \(\d+ steps\)/
|
37
|
+
@awaiting_scenario_name = true
|
38
|
+
|
39
|
+
when /END OF SCENARIO \(duration (\d+\.\d+)s/
|
40
|
+
@report.in_current_suite do |suite|
|
41
|
+
suite.finish(Time.now)
|
42
|
+
end
|
43
|
+
|
44
|
+
when /(PASS|FAIL) \((\d+\.\d+s)\): (.+)/
|
45
|
+
duration = $2.to_f
|
46
|
+
name = $3.strip
|
47
|
+
@report.in_current_suite do |suite|
|
48
|
+
test = suite.add_test_case(name)
|
49
|
+
|
50
|
+
if $1 == 'PASS'
|
51
|
+
test.passed(duration)
|
52
|
+
else
|
53
|
+
test.failed(duration)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
when /KIF TEST RUN FINISHED: \d+ failures \(duration (\d+\.\d+)s\)/
|
58
|
+
@report.finish
|
59
|
+
|
60
|
+
when /(.*): error: -\[(\S+) (\S+)\] : (.*)/
|
61
|
+
message = $4
|
62
|
+
location = $1
|
63
|
+
@report.in_current_test do |test|
|
64
|
+
test.add_error(message, location)
|
65
|
+
end
|
66
|
+
|
67
|
+
# when /failed with exit code (\d+)/,
|
68
|
+
when /BUILD FAILED/
|
69
|
+
@report.finish
|
70
|
+
|
71
|
+
when /Segmentation fault/
|
72
|
+
@report.abort
|
73
|
+
|
74
|
+
when /the iPhoneSimulator platform does not currently support application-hosted tests/
|
75
|
+
raise "Application tests are not currently supported by the iphone simulator. If these are logic tests, try unsetting TEST_HOST in your project config"
|
76
|
+
else
|
77
|
+
@report.in_current_test do |test|
|
78
|
+
test << piped_row
|
79
|
+
end
|
80
|
+
end # case
|
81
|
+
|
82
|
+
end # <<
|
83
|
+
|
84
|
+
end # OCUnitParser
|
85
|
+
end # Parsers
|
86
|
+
end # Test
|
87
|
+
end # Xcode
|
@@ -7,17 +7,17 @@ module Xcode
|
|
7
7
|
|
8
8
|
class OCUnitParser
|
9
9
|
attr_accessor :report, :builder
|
10
|
-
|
10
|
+
|
11
11
|
def initialize(report = Xcode::Test::Report.new)
|
12
12
|
@report = report
|
13
13
|
yield self if block_given?
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def close
|
17
17
|
@report.finish
|
18
18
|
end
|
19
19
|
|
20
|
-
def <<
|
20
|
+
def << piped_row
|
21
21
|
case piped_row.force_encoding("UTF-8")
|
22
22
|
|
23
23
|
when /Test Suite '(\S+)'.*started at\s+(.*)/
|
data/lib/xcode/version.rb
CHANGED
data/lib/xcode/workspace.rb
CHANGED
@@ -15,9 +15,13 @@ module Xcode
|
|
15
15
|
doc = Nokogiri::XML(open("#{@path}/contents.xcworkspacedata"))
|
16
16
|
doc.search("FileRef").each do |file|
|
17
17
|
location = file["location"]
|
18
|
-
if matcher = location.match(/^group:(
|
18
|
+
if (matcher = location.match(/^group:(.+\.xcodeproj)$/i))
|
19
19
|
project_path = "#{workspace_root}/#{matcher[1]}"
|
20
|
-
|
20
|
+
begin
|
21
|
+
@projects << Xcode::Project.new(project_path)
|
22
|
+
rescue => e
|
23
|
+
puts "Skipping project file #{project_path} referened by #{self} as it failed to parse"
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -41,8 +45,8 @@ module Xcode
|
|
41
45
|
# @return [Scheme] the specific scheme that matches the name specified
|
42
46
|
#
|
43
47
|
def scheme(name)
|
44
|
-
scheme = schemes.select {|t| t.name == name.to_s}.first
|
45
|
-
raise "No such scheme #{name}, available schemes are #{schemes.map {|t| t.
|
48
|
+
scheme = schemes.select {|t| t.name == name.to_s and t.parent == self}.first
|
49
|
+
raise "No such scheme #{name} in #{self}, available schemes are #{schemes.map {|t| t.to_s}.join(', ')}" if scheme.nil?
|
46
50
|
yield scheme if block_given?
|
47
51
|
scheme
|
48
52
|
end
|
@@ -58,10 +62,14 @@ module Xcode
|
|
58
62
|
#
|
59
63
|
def project(name)
|
60
64
|
project = @projects.select {|c| c.name == name.to_s}.first
|
61
|
-
raise "No such project #{name}, available projects are #{@projects.map {|c| c.name}.join(', ')}" if project.nil?
|
65
|
+
raise "No such project #{name} in #{self}, available projects are #{@projects.map {|c| c.name}.join(', ')}" if project.nil?
|
62
66
|
yield project if block_given?
|
63
67
|
project
|
64
68
|
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
"#{name} (Workspace)"
|
72
|
+
end
|
65
73
|
|
66
74
|
def describe
|
67
75
|
puts "Workspace #{name} contains:"
|
data/lib/xcoder.rb
CHANGED
@@ -1,90 +1,92 @@
|
|
1
1
|
require 'find'
|
2
2
|
require 'fileutils'
|
3
|
+
require 'xcode/terminal_output'
|
3
4
|
require "xcode/version"
|
4
5
|
require "xcode/project"
|
5
6
|
require "xcode/info_plist"
|
6
|
-
require
|
7
|
+
require 'xcode/shell/command'
|
7
8
|
require 'plist'
|
8
9
|
require 'xcode/keychain'
|
9
10
|
require 'xcode/workspace'
|
10
|
-
require 'xcode/
|
11
|
+
require 'xcode/platform'
|
12
|
+
require 'multi_json'
|
11
13
|
|
12
14
|
module Xcode
|
13
|
-
|
15
|
+
|
14
16
|
@@projects = nil
|
15
17
|
@@workspaces = nil
|
16
18
|
@@sdks = nil
|
17
|
-
|
19
|
+
|
18
20
|
#
|
19
21
|
# Find all the projects within the current working directory.
|
20
|
-
#
|
22
|
+
#
|
21
23
|
# @return [Array<Project>] an array of the all the Projects found.
|
22
|
-
#
|
24
|
+
#
|
23
25
|
def self.projects
|
24
26
|
@@projects = parse_projects if @@projects.nil?
|
25
|
-
@@projects
|
27
|
+
@@projects
|
26
28
|
end
|
27
|
-
|
29
|
+
|
28
30
|
#
|
29
31
|
# Find all the workspaces within the current working directory.
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# @return [Array<Workspaces>] an array of all the Workspaces found.
|
32
|
-
#
|
34
|
+
#
|
33
35
|
def self.workspaces
|
34
36
|
@@workspaces = parse_workspaces if @@workspaces.nil?
|
35
37
|
@@workspaces
|
36
38
|
end
|
37
|
-
|
39
|
+
|
38
40
|
#
|
39
41
|
# Find the project with the specified name within the current working directory.
|
40
|
-
#
|
42
|
+
#
|
41
43
|
# @note this method will raise an error when it is unable to find the project
|
42
44
|
# specified.
|
43
|
-
#
|
45
|
+
#
|
44
46
|
# @param [String] name of the project (e.g. NAME.xcodeproj) that is attempting
|
45
47
|
# to be found.
|
46
|
-
#
|
48
|
+
#
|
47
49
|
# @return [Project] the project found; an error is raise if a project is unable
|
48
50
|
# to be found.
|
49
|
-
#
|
51
|
+
#
|
50
52
|
def self.project(name)
|
51
53
|
name = name.to_s
|
52
|
-
|
54
|
+
|
53
55
|
return Xcode::Project.new(name) if name=~/\.xcodeproj/
|
54
|
-
|
56
|
+
|
55
57
|
self.projects.each do |p|
|
56
58
|
return p if p.name == name
|
57
59
|
end
|
58
60
|
raise "Unable to find a project named #{name}. However, I did find these projects: #{self.projects.map {|p| p.name}.join(', ') }"
|
59
61
|
end
|
60
|
-
|
62
|
+
|
61
63
|
#
|
62
64
|
# Find the workspace with the specified name within the current working directory.
|
63
|
-
#
|
65
|
+
#
|
64
66
|
# @note this method will raise an error when it is unable to find the workspace
|
65
67
|
# specified.
|
66
|
-
#
|
68
|
+
#
|
67
69
|
# @param [String] name of the workspace (e.g. NAME.xcworkspace) that is attempting
|
68
70
|
# to be found.
|
69
|
-
#
|
71
|
+
#
|
70
72
|
# @return [Project] the workspace found; an error is raise if a workspace is unable
|
71
73
|
# to be found.
|
72
|
-
#
|
74
|
+
#
|
73
75
|
def self.workspace(name)
|
74
76
|
name = name.to_s
|
75
|
-
|
77
|
+
|
76
78
|
return Xcode::Workspace.new(name) if name=~/\.xcworkspace/
|
77
|
-
|
79
|
+
|
78
80
|
self.workspaces.each do |p|
|
79
81
|
return p if p.name == name
|
80
82
|
end
|
81
83
|
raise "Unable to find a workspace named #{name}. However, I did find these workspaces: #{self.workspaces.map {|p| p.name}.join(', ') }"
|
82
84
|
end
|
83
|
-
|
85
|
+
|
84
86
|
#
|
85
87
|
# @param [String] dir the path to search for projects; defaults to using
|
86
88
|
# the current working directory.
|
87
|
-
#
|
89
|
+
#
|
88
90
|
# @return [Array<Project>] the projects found at the specified directory.
|
89
91
|
#
|
90
92
|
def self.find_projects(dir='.')
|
@@ -99,17 +101,17 @@ module Xcode
|
|
99
101
|
parse_sdks if @@sdks.nil?
|
100
102
|
@@sdks.values.include? sdk
|
101
103
|
end
|
102
|
-
|
104
|
+
|
103
105
|
#
|
104
106
|
# Available SDKs available on this particular system.
|
105
|
-
#
|
107
|
+
#
|
106
108
|
# @return [Array<String>] the available SDKs on the current system.
|
107
|
-
#
|
109
|
+
#
|
108
110
|
def self.available_sdks
|
109
111
|
parse_sdks if @@sdks.nil?
|
110
112
|
@@sdks
|
111
113
|
end
|
112
|
-
|
114
|
+
|
113
115
|
private
|
114
116
|
def self.parse_sdks
|
115
117
|
@@sdks = {}
|
@@ -126,7 +128,7 @@ module Xcode
|
|
126
128
|
end
|
127
129
|
end
|
128
130
|
end
|
129
|
-
|
131
|
+
|
130
132
|
def self.parse_workspaces(dir='.')
|
131
133
|
projects = []
|
132
134
|
Find.find(dir) do |path|
|