hoopla_salesforce 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +114 -0
- data/bin/hoopla_salesforce +8 -0
- data/lib/hoopla_salesforce/commands.rb +21 -0
- data/lib/hoopla_salesforce/deployer.rb +1 -1
- data/lib/hoopla_salesforce/eruby.rb +54 -0
- data/lib/hoopla_salesforce/ext/commander.rb +7 -0
- data/lib/hoopla_salesforce/ext/string.rb +14 -0
- data/lib/hoopla_salesforce/info.rb +19 -0
- data/lib/hoopla_salesforce/package_generator.rb +130 -0
- data/lib/hoopla_salesforce/rake/deploy_task.rb +201 -28
- data/lib/hoopla_salesforce/rake/retrieve_task.rb +6 -4
- data/lib/hoopla_salesforce/skeleton.rb +121 -0
- data/lib/hoopla_salesforce/template_processor.rb +120 -0
- data/lib/hoopla_salesforce/text_reporter.rb +3 -2
- data/lib/hoopla_salesforce/utils.rb +11 -0
- data/lib/hoopla_salesforce/web_agent.rb +71 -0
- metadata +61 -8
- data/lib/hoopla_salesforce/version.rb +0 -3
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
hoopla_salesforce
|
2
|
+
=================
|
3
|
+
|
4
|
+
A nitfy bundle of dev and deployment tools for working on the (Sales)force.com platform.
|
5
|
+
|
6
|
+
Features
|
7
|
+
========
|
8
|
+
|
9
|
+
1. Generates new empty projects
|
10
|
+
2. Deploys your project with Rake
|
11
|
+
3. Generates *-meta.xml for any files that are missing it
|
12
|
+
4. Runs tests on deploy and prints results in ANSI color
|
13
|
+
5. Auto-packages folders in src/resource as staticresources
|
14
|
+
6. Undeploys your project (destructiveChanges.xml is auto-generated)
|
15
|
+
7. Pre-processes any VisualForce or Apex files with ERB
|
16
|
+
8. Generates static test pages from VisualForce pages
|
17
|
+
9. Runs tests via the Web UI (as results often differ from deploy tests)
|
18
|
+
10. Makes pancakes (pending)
|
19
|
+
|
20
|
+
Usage
|
21
|
+
=====
|
22
|
+
|
23
|
+
Install it with:
|
24
|
+
|
25
|
+
(sudo) gem install hoopla_salesforce
|
26
|
+
|
27
|
+
There are a lot of features in here, so the rest of this document will go over how they work.
|
28
|
+
|
29
|
+
Generating new projects
|
30
|
+
-----------------------
|
31
|
+
|
32
|
+
Just run:
|
33
|
+
|
34
|
+
hoopla_salesforce init myproject
|
35
|
+
|
36
|
+
This will create an folder called myproject that looks like this:
|
37
|
+
|
38
|
+
- myproject
|
39
|
+
|- Rakefile
|
40
|
+
|- lib/
|
41
|
+
\- src/
|
42
|
+
|- applications/
|
43
|
+
|- classes/
|
44
|
+
|- objects/
|
45
|
+
|- pages/
|
46
|
+
|- resources/
|
47
|
+
|- tabs/
|
48
|
+
|- triggers/
|
49
|
+
\- package.xml
|
50
|
+
|
51
|
+
Once this is done, drop the enterprise.xml and metadata.xml for your org into `lib/` and update the `Rakefile` to reflect your username, password and security token. Now your project is ready to deploy. Run `rake -T` to show the available rake tasks.
|
52
|
+
|
53
|
+
Deploy your projects with rake
|
54
|
+
------------------------------
|
55
|
+
|
56
|
+
Deploy your project using `rake hsf:deploy:development`. Or if you change the environment name of your DeployTask, use whatever you specified in place of 'development'.
|
57
|
+
|
58
|
+
If you need to see the full deployment output run it with `FULL_OUTPUT=true`. Currently coverage information is only available in the full output. The default output will include colorized information about what's been added, updated, tests run, any failures or compilation problems.
|
59
|
+
|
60
|
+
Generating meta.xml files
|
61
|
+
-------------------------
|
62
|
+
|
63
|
+
During deployment, we'll generate any missing meta.xml files for your assets. This currently supports generation for classes, pages, documents, static resources, and triggers. If you already have meta.xml files for your resources, the gem will skip those and include your files for deployment.
|
64
|
+
|
65
|
+
Autopackaging of static resources
|
66
|
+
---------------------------------
|
67
|
+
|
68
|
+
As part of deployment, any folders in src/resources will get zipped up as static resources. So for example if you had a folder `src/resources/Performance` the contents of this folder will get zipped into `src/static/resources/Performance.resource`. This makes dealing with zipped CSS/JavaScript much easier.
|
69
|
+
|
70
|
+
Undeploying your project
|
71
|
+
------------------------
|
72
|
+
|
73
|
+
To undeploy your project run `rake hsf:undeploy:development`. This will scan your src folder and generate the appropriate destructiveChanges.xml file used by Salesforce to undeploy your code.
|
74
|
+
|
75
|
+
Note that undeployed fields are still left in the "deleted fields" section of the site. Once you have two copies of a deleted field, you can't re-deploy that field without first cleaning up the deleted fields via the WebUI. Automatic erasing of fields is a planned feature to avoid this headache.
|
76
|
+
|
77
|
+
Pre-processing with ERB
|
78
|
+
-----------------------
|
79
|
+
|
80
|
+
If you append `.erb` to any file in your project, it will get processed through Erubis prior to deployment. This makes it possible to DRY up a lot of your XML, abstract common patterns and even write simple Apex macros.
|
81
|
+
|
82
|
+
Just before the templates are run, the deployer will look for `lib/template_helper.rb` and load that. In this file you can mix your own methods into the template processors. The following example allows you to use `<%= name 'MyObject' %>` in an object definition to avoid writing the XML boilerplate that's required by salesforce.
|
83
|
+
|
84
|
+
module GenericHelper
|
85
|
+
def object(&block)
|
86
|
+
code = <<-XML.margin
|
87
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
88
|
+
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
|
89
|
+
<deploymentStatus>Deployed</deploymentStatus>
|
90
|
+
<sharingModel>ReadWrite</sharingModel>
|
91
|
+
#{capture &block}
|
92
|
+
</CustomObject>
|
93
|
+
XML
|
94
|
+
end
|
95
|
+
end
|
96
|
+
HooplaSalesforce::TemplateProcessor::Generic.send(:include, GenericHelper)
|
97
|
+
|
98
|
+
Note that you must currently mix your module into the processors at the bottom of the file. The currently available processors are:
|
99
|
+
|
100
|
+
* `HooplaSalesforce::TemplateProcessor::VisualForce` - used for processing page files
|
101
|
+
* `HooplaSalesforce::TemplateProcessor::TestPage` - used for generating static test pages
|
102
|
+
* `HooplaSalesforce::TemplateProcessor::Generic` - used for any other files
|
103
|
+
|
104
|
+
Generating Static Test Pages
|
105
|
+
----------------------------
|
106
|
+
|
107
|
+
(documentation pending)
|
108
|
+
|
109
|
+
Running tests
|
110
|
+
-------------
|
111
|
+
|
112
|
+
Use WEB_TEST=true to run via web ui. Default will run during deployment. Use TEST_NAMES=Test1,Test2,etc to run specific tests (or TEST_NAMES="" to skip tests).
|
113
|
+
|
114
|
+
(more documentation pending)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'hoopla_salesforce/info'
|
2
|
+
require 'commander/import'
|
3
|
+
require 'hoopla_salesforce/ext/commander'
|
4
|
+
|
5
|
+
program :name, HooplaSalesforce.name
|
6
|
+
program :version, HooplaSalesforce.version
|
7
|
+
program :description, HooplaSalesforce.summary
|
8
|
+
|
9
|
+
default_command :help
|
10
|
+
|
11
|
+
command "init" do |c|
|
12
|
+
c.syntax = '[options] <directory>'
|
13
|
+
c.summary = 'Creates a skeleton app.'
|
14
|
+
c.description = 'Creates a skeleton Salesforce.com app in the specified directory.'
|
15
|
+
|
16
|
+
c.action do |args, options|
|
17
|
+
directory = args.first or raise "Directory name required."
|
18
|
+
require 'hoopla_salesforce/skeleton'
|
19
|
+
HooplaSalesforce::Skeleton.new(directory).create
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'erubis'
|
2
|
+
|
3
|
+
module HooplaSalesforce
|
4
|
+
class OutputBuffer < String
|
5
|
+
alias :append= :<<
|
6
|
+
alias :safe_concat :<<
|
7
|
+
end
|
8
|
+
|
9
|
+
module CaptureHelper
|
10
|
+
def capture
|
11
|
+
@output_buffer, original_buffer = HooplaSalesforce::OutputBuffer.new, @output_buffer
|
12
|
+
yield
|
13
|
+
@output_buffer, buffer = original_buffer, @output_buffer
|
14
|
+
buffer
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Eruby < Erubis::Eruby
|
19
|
+
def add_preamble(src)
|
20
|
+
src << "@output_buffer = HooplaSalesforce::OutputBuffer.new;"
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_text(src, text)
|
24
|
+
return if text.empty?
|
25
|
+
src << "@output_buffer.safe_concat('" << escape_text(text) << "');"
|
26
|
+
end
|
27
|
+
|
28
|
+
BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
|
29
|
+
|
30
|
+
def add_expr_literal(src, code)
|
31
|
+
if code =~ BLOCK_EXPR
|
32
|
+
src << '@output_buffer.append= ' << code
|
33
|
+
else
|
34
|
+
src << '@output_buffer.append= (' << code << ');'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_stmt(src, code)
|
39
|
+
if code =~ BLOCK_EXPR
|
40
|
+
src << code
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_expr_escaped(src, code)
|
47
|
+
src << '@output_buffer.append= ' << escaped_expr(code) << ';'
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_postamble(src)
|
51
|
+
src << '@output_buffer.to_s'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class String
|
2
|
+
def margin
|
3
|
+
spaces = match(/^\s*/)
|
4
|
+
gsub(/^#{spaces[0]}/, '')
|
5
|
+
end
|
6
|
+
|
7
|
+
def camelize(first_letter_in_uppercase = true)
|
8
|
+
if first_letter_in_uppercase
|
9
|
+
gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
10
|
+
else
|
11
|
+
first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'hoopla_salesforce/utils'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module HooplaSalesforce
|
5
|
+
class PackageGenerator
|
6
|
+
include Utils
|
7
|
+
|
8
|
+
attr_reader :processed_src
|
9
|
+
attr_reader :api_version
|
10
|
+
|
11
|
+
# Right now this only works with making a destructiveChanges.xml
|
12
|
+
# Need to add support namespace, etc before using to generate package.xml
|
13
|
+
def initialize(processed_src, api_version)
|
14
|
+
@processed_src = processed_src
|
15
|
+
@api_version = api_version
|
16
|
+
end
|
17
|
+
|
18
|
+
def supported_types
|
19
|
+
%w(apex_class apex_page custom_tab custom_application apex_trigger custom_field
|
20
|
+
static_resource)
|
21
|
+
end
|
22
|
+
|
23
|
+
def map_files(glob, &block)
|
24
|
+
Dir["#{processed_src}/#{glob}"].map(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def members_for_apex_class
|
28
|
+
map_files("classes/*.cls") do |klass|
|
29
|
+
data = File.read(klass)
|
30
|
+
extract_class_name(data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def members_for_apex_trigger
|
35
|
+
map_files("triggers/*.trigger") do |trigger|
|
36
|
+
data = File.read(trigger)
|
37
|
+
extract_trigger_name(data)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def members_for_apex_page
|
42
|
+
map_files("pages/*.page") do |page|
|
43
|
+
File.basename(page, '.page')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def members_for_custom_tab
|
48
|
+
map_files("tabs/*.tab") do |tab|
|
49
|
+
File.basename(tab, '.tab')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def members_for_custom_application
|
54
|
+
map_files("applications/*.app") do |app|
|
55
|
+
File.read(app).match(/<fullName>([^<]*)<\/fullName>/)[1]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def members_for_custom_field
|
60
|
+
map_files("objects/*.object") do |object|
|
61
|
+
obj_name = File.basename(object, ".object")
|
62
|
+
data = File.read(object)
|
63
|
+
xml_doc = Nokogiri::XML(data)
|
64
|
+
xml_doc.search("fullName").map { |f| [obj_name, f.text].join('.') }
|
65
|
+
end.flatten
|
66
|
+
end
|
67
|
+
|
68
|
+
def members_for_static_resource
|
69
|
+
map_files("staticresources/*.resource") do |resource|
|
70
|
+
File.basename(resource, ".resource")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def empty_package_xml
|
75
|
+
<<-EOS.margin
|
76
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
77
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
78
|
+
<version>#{api_version}</version>
|
79
|
+
</Package>
|
80
|
+
EOS
|
81
|
+
end
|
82
|
+
|
83
|
+
def destructive_changes_xml
|
84
|
+
<<-EOS.margin
|
85
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
86
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
87
|
+
#{package_types}
|
88
|
+
<version>#{api_version}</version>
|
89
|
+
</Package>
|
90
|
+
EOS
|
91
|
+
end
|
92
|
+
|
93
|
+
def package_types
|
94
|
+
supported_types.map do |type|
|
95
|
+
members = send("members_for_#{type}")
|
96
|
+
unless members.empty?
|
97
|
+
members_xml = members.map { |m| "<members>#{m}</members>" }.join("\n")
|
98
|
+
<<-EOS.margin
|
99
|
+
<types>
|
100
|
+
#{members_xml}
|
101
|
+
<name>#{type.camelize}</name>
|
102
|
+
</types>
|
103
|
+
EOS
|
104
|
+
end
|
105
|
+
end.join("\n")
|
106
|
+
end
|
107
|
+
|
108
|
+
def generate_destructive_changes
|
109
|
+
write_xml "package.xml", empty_package_xml
|
110
|
+
write_xml "destructiveChanges.xml", destructive_changes_xml
|
111
|
+
remove_processed_files_for_undeploy
|
112
|
+
end
|
113
|
+
|
114
|
+
def write_xml(file, data)
|
115
|
+
File.open("#{processed_src}/#{file}", 'w') do |pkg|
|
116
|
+
pkg.print data
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def protected_files_for_destruction
|
121
|
+
%W(#{processed_src}/package.xml #{processed_src}/destructiveChanges.xml)
|
122
|
+
end
|
123
|
+
|
124
|
+
def remove_processed_files_for_undeploy
|
125
|
+
Dir["#{processed_src}/*"].each do |file|
|
126
|
+
FileUtils.rm_rf(file) unless protected_files_for_destruction.include?(file)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'hoopla_salesforce/rake/base_task'
|
2
|
+
require 'hoopla_salesforce/ext/string'
|
3
|
+
require 'hoopla_salesforce/template_processor'
|
4
|
+
require 'hoopla_salesforce/utils'
|
2
5
|
|
3
6
|
module HooplaSalesforce
|
4
7
|
module Rake
|
@@ -11,6 +14,8 @@ module HooplaSalesforce
|
|
11
14
|
# - Zips up any folders found in src/resources as src/staticresources/#{folder}.resource.
|
12
15
|
# This allows you to only keep the raw assets in your project
|
13
16
|
class DeployTask < BaseTask
|
17
|
+
include Utils
|
18
|
+
|
14
19
|
# Your project root. Defaults to 'src'
|
15
20
|
attr_accessor :src
|
16
21
|
|
@@ -23,50 +28,195 @@ module HooplaSalesforce
|
|
23
28
|
# the namespaced package) and a dev org (that does not have one).
|
24
29
|
#
|
25
30
|
# default - nil
|
26
|
-
attr_accessor :
|
31
|
+
attr_accessor :package_namespace
|
27
32
|
|
28
33
|
# The directory in which to store processed source files.
|
29
34
|
# Defaults to #{src}-processed
|
30
35
|
attr_accessor :processed_src
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
37
|
+
# Which file to load before processing templates. Default: lib/template_helper.rb
|
38
|
+
# To have this file inject methods into the HooplaSalesforce::TemplateProcessor,
|
39
|
+
# be sure to include your module in HooplaSalesforce::TemplateProcessor::VisualForce or
|
40
|
+
# HooplaSalesforce::TemplateProcessor::TestPage at the bottom of your template_helper
|
41
|
+
# file.
|
42
|
+
attr_accessor :template_helper
|
43
|
+
|
44
|
+
def initialize(name=:production)
|
45
|
+
@deploy_file = "deploy.zip"
|
46
|
+
@src = 'src'
|
47
|
+
@package_namespace = nil
|
48
|
+
@template_helper = 'lib/template_helper.rb'
|
36
49
|
super
|
37
|
-
@
|
38
|
-
@processed_src
|
50
|
+
@package_namespace += "__" if @package_namespace
|
51
|
+
@processed_src ||= "#{src}-processed"
|
52
|
+
end
|
53
|
+
|
54
|
+
def clean_namespace
|
55
|
+
package_namespace.sub(/__$/, '') if package_namespace
|
39
56
|
end
|
40
57
|
|
41
58
|
def define
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
59
|
+
if name.is_a? Hash
|
60
|
+
task_name = name.keys.first
|
61
|
+
dependencies = name[task_name]
|
62
|
+
else
|
63
|
+
task_name = name
|
64
|
+
dependencies = []
|
65
|
+
end
|
66
|
+
|
67
|
+
namespace :hsf do
|
68
|
+
namespace :undeploy do
|
69
|
+
desc "Undeploy all components in #{src} from salesforce"
|
70
|
+
task task_name => dependencies do
|
71
|
+
process_source
|
72
|
+
require 'hoopla_salesforce/package_generator'
|
73
|
+
PackageGenerator.new(processed_src, api_version).generate_destructive_changes
|
74
|
+
make_zipfile
|
75
|
+
deploy_zipfile
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
namespace :deploy do
|
80
|
+
desc "Deploy to salesforce"
|
81
|
+
task task_name => dependencies do
|
82
|
+
process_source
|
83
|
+
make_meta_xmls
|
84
|
+
make_zipfile
|
85
|
+
deploy_zipfile
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "Renders any page templates as test pages in #{processed_src}/pages-test"
|
90
|
+
task :testpages => dependencies do
|
91
|
+
mkdir_p "#{src}/pages-test"
|
92
|
+
make_pages do |template|
|
93
|
+
HooplaSalesforce::TemplateProcessor::TestPage.new(src, template)
|
94
|
+
end
|
95
|
+
end
|
49
96
|
end
|
50
97
|
end
|
51
98
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
99
|
+
def test_names
|
100
|
+
if ENV['TEST_NAMES']
|
101
|
+
ENV['TEST_NAMES'].split(',')
|
102
|
+
else
|
103
|
+
Dir["#{processed_src}/classes/*.cls"].inject([]) do |names, f|
|
104
|
+
body = File.read(f)
|
105
|
+
if body =~ /(testMethod|@isTest)/ && name = extract_class_name(body)
|
106
|
+
names << name
|
107
|
+
else
|
108
|
+
names
|
109
|
+
end
|
59
110
|
end
|
60
111
|
end
|
112
|
+
end
|
61
113
|
|
62
|
-
|
114
|
+
def test_options
|
115
|
+
if test_names.empty?
|
63
116
|
{ "wsdl:runAllTests" => true }
|
64
117
|
else
|
65
|
-
|
118
|
+
test_names.map! { |n| "#{clean_namespace}.#{n}" } if package_namespace
|
66
119
|
{ "wsdl:runTests" => testNames }
|
67
120
|
end
|
68
121
|
end
|
69
122
|
|
123
|
+
def api_version
|
124
|
+
return @api_version if @api_version
|
125
|
+
File.read("#{src}/package.xml") =~ /<version>(.*)<\/version>/i
|
126
|
+
@api_version = $1
|
127
|
+
end
|
128
|
+
|
129
|
+
def make_pages
|
130
|
+
require template_helper if File.exist?(template_helper)
|
131
|
+
|
132
|
+
Dir["#{src}/pages/*.page.erb"].each do |template|
|
133
|
+
yield template
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def preprocess_files
|
138
|
+
require template_helper if File.exist?(template_helper)
|
139
|
+
|
140
|
+
Dir["#{processed_src}/**/*.erb"].each do |template|
|
141
|
+
yield template
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def make_meta(glob)
|
146
|
+
Dir["#{processed_src}/#{glob}"].each do |file|
|
147
|
+
meta = "#{file}-meta.xml"
|
148
|
+
next if file =~ /-meta\.xml$/ || File.exist?(meta)
|
149
|
+
File.open(meta, 'w') do |f|
|
150
|
+
f.print yield(file)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def make_meta_xmls
|
156
|
+
make_meta "classes/*.cls" do |klass|
|
157
|
+
<<-EOS.margin
|
158
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
159
|
+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
160
|
+
<apiVersion>#{api_version}</apiVersion>
|
161
|
+
</ApexClass>
|
162
|
+
EOS
|
163
|
+
end
|
164
|
+
|
165
|
+
make_meta "pages/*.page" do |page|
|
166
|
+
<<-EOS.margin
|
167
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
168
|
+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
169
|
+
<apiVersion>#{api_version}</apiVersion>
|
170
|
+
<label>#{File.basename(page, '.page')}</label>
|
171
|
+
</ApexClass>
|
172
|
+
EOS
|
173
|
+
end
|
174
|
+
|
175
|
+
make_meta "documents/**/*" do |doc|
|
176
|
+
if File.directory?(doc)
|
177
|
+
m = <<-EOS.margin
|
178
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
179
|
+
<DocumentFolder xmlns="http://soap.sforce.com/2006/04/metadata">
|
180
|
+
<name>#{File.basename(doc)}</name>
|
181
|
+
<accessType>Public</accessType>
|
182
|
+
<publicFolderAccess>ReadOnly</publicFolderAccess>
|
183
|
+
</DocumentFolder>
|
184
|
+
EOS
|
185
|
+
else
|
186
|
+
<<-EOS.margin
|
187
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
188
|
+
<Document xmlns="http://soap.sforce.com/2006/04/metadata">
|
189
|
+
<internalUseOnly>false</internalUseOnly>
|
190
|
+
<name>#{File.basename(doc)}</name>
|
191
|
+
<public>true</public>
|
192
|
+
<description>A document</description>
|
193
|
+
</Document>
|
194
|
+
EOS
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
make_meta "staticresources/*.resource" do |resource|
|
199
|
+
<<-EOS.margin
|
200
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
201
|
+
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
|
202
|
+
<cacheControl>Private</cacheControl>
|
203
|
+
<contentType>application/zip</contentType>
|
204
|
+
<description>A static resource</description>
|
205
|
+
</StaticResource>
|
206
|
+
EOS
|
207
|
+
end
|
208
|
+
|
209
|
+
make_meta "triggers/*.trigger" do |trigger|
|
210
|
+
<<-EOS.margin
|
211
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
212
|
+
<ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata">
|
213
|
+
<apiVersion>#{api_version}</apiVersion>
|
214
|
+
<status>Active</status>
|
215
|
+
</ApexTrigger>
|
216
|
+
EOS
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
70
220
|
def make_resources
|
71
221
|
require 'zip/zip'
|
72
222
|
|
@@ -83,7 +233,8 @@ module HooplaSalesforce
|
|
83
233
|
end
|
84
234
|
end
|
85
235
|
|
86
|
-
|
236
|
+
source_meta = "#{f}.resource-meta.xml"
|
237
|
+
cp source_meta, "#{resourcefile}-meta.xml" if File.exist?(source_meta)
|
87
238
|
end
|
88
239
|
end
|
89
240
|
|
@@ -97,17 +248,39 @@ module HooplaSalesforce
|
|
97
248
|
end
|
98
249
|
end
|
99
250
|
|
251
|
+
def deploy_zipfile
|
252
|
+
require 'hoopla_salesforce/deployer'
|
253
|
+
deployer = HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl)
|
254
|
+
if ENV['WEB_TESTS']
|
255
|
+
require 'hoopla_salesforce/web_agent'
|
256
|
+
deployer.deploy(deploy_file)
|
257
|
+
WebAgent.new(username, password).run_tests(test_names, clean_namespace)
|
258
|
+
else
|
259
|
+
deployer.deploy(deploy_file, test_options)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
100
263
|
def process_source
|
101
264
|
rm_rf processed_src
|
102
265
|
cp_r src, processed_src
|
103
266
|
Dir["#{processed_src}/**/*"].each do |f|
|
104
267
|
next if File.directory?(f)
|
105
|
-
system %Q|ruby -i -n -e 'print $_.gsub("__NAMESPACE__", "#{
|
268
|
+
system %Q|ruby -i -n -e 'print $_.gsub("__NAMESPACE__", "#{package_namespace}")' "#{f}"|
|
269
|
+
end
|
270
|
+
|
271
|
+
if package_namespace
|
272
|
+
system %Q|ruby -i -n -e 'print $_.gsub("#{package_namespace}", "#{clean_namespace}")' "#{processed_src}/package.xml"|
|
273
|
+
end
|
274
|
+
|
275
|
+
make_resources
|
276
|
+
make_pages do |template|
|
277
|
+
HooplaSalesforce::TemplateProcessor::VisualForce.new(processed_src, template)
|
278
|
+
rm template.sub(src, processed_src)
|
106
279
|
end
|
107
280
|
|
108
|
-
|
109
|
-
|
110
|
-
|
281
|
+
preprocess_files do |template|
|
282
|
+
HooplaSalesforce::TemplateProcessor::Generic.new(processed_src, template)
|
283
|
+
rm template
|
111
284
|
end
|
112
285
|
end
|
113
286
|
end
|
@@ -20,10 +20,12 @@ module HooplaSalesforce
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def define
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
namespace :hsf do
|
24
|
+
desc "Retrieve all apex classes from salesforce.com"
|
25
|
+
task name do
|
26
|
+
require 'hoopla_salesforce/deployer'
|
27
|
+
HooplaSalesforce::Deployer.new(username, password, token, enterprise_wsdl, metadata_wsdl).retrieve(request)
|
28
|
+
end
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'hoopla_salesforce/ext/string'
|
3
|
+
|
4
|
+
module HooplaSalesforce
|
5
|
+
class Skeleton
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
attr_reader :directory
|
9
|
+
|
10
|
+
def initialize(directory)
|
11
|
+
@directory = directory
|
12
|
+
end
|
13
|
+
|
14
|
+
def mkdir_p(dir='')
|
15
|
+
super directory + dir
|
16
|
+
end
|
17
|
+
|
18
|
+
def create
|
19
|
+
puts "Creating a skeleton in #{directory}"
|
20
|
+
%w(applications classes objects pages resources tabs triggers).each do |dir|
|
21
|
+
mkdir_p "/src/#{dir}"
|
22
|
+
end
|
23
|
+
mkdir_p '/lib'
|
24
|
+
make_package_xml
|
25
|
+
make_rakefile
|
26
|
+
make_gitignore
|
27
|
+
followup_instructions
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_package_xml
|
31
|
+
File.open("#{directory}/src/package.xml", 'w') do |pkg|
|
32
|
+
pkg.print <<-EOS.margin
|
33
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
34
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
35
|
+
<fullName></fullName>
|
36
|
+
<apiAccessLevel>Unrestricted</apiAccessLevel>
|
37
|
+
<description></description>
|
38
|
+
<namespacePrefix>__NAMESPACE__</namespacePrefix>
|
39
|
+
<types>
|
40
|
+
<members>*</members>
|
41
|
+
<name>ApexClass</name>
|
42
|
+
</types>
|
43
|
+
<types>
|
44
|
+
<members>*</members>
|
45
|
+
<name>ApexPage</name>
|
46
|
+
</types>
|
47
|
+
<types>
|
48
|
+
<members>*</members>
|
49
|
+
<name>ApexTrigger</name>
|
50
|
+
</types>
|
51
|
+
<types>
|
52
|
+
<members>*</members>
|
53
|
+
<name>CustomApplication</name>
|
54
|
+
</types>
|
55
|
+
<types>
|
56
|
+
<members>*</members>
|
57
|
+
<name>CustomObject</name>
|
58
|
+
</types>
|
59
|
+
<types>
|
60
|
+
<members>*</members>
|
61
|
+
<name>CustomField</name>
|
62
|
+
</types>
|
63
|
+
<types>
|
64
|
+
<members>*</members>
|
65
|
+
<name>ValidationRule</name>
|
66
|
+
</types>
|
67
|
+
<types>
|
68
|
+
<members>*</members>
|
69
|
+
<name>CustomTab</name>
|
70
|
+
</types>
|
71
|
+
<types>
|
72
|
+
<members>*</members>
|
73
|
+
<name>StaticResource</name>
|
74
|
+
</types>
|
75
|
+
<version>18.0</version>
|
76
|
+
</Package>
|
77
|
+
EOS
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def make_rakefile
|
82
|
+
File.open("#{directory}/Rakefile", 'w') do |rakefile|
|
83
|
+
rakefile.print <<-EOS.margin
|
84
|
+
require 'hoopla_salesforce/rake'
|
85
|
+
|
86
|
+
HooplaSalesforce.enterprise_wsdl = "lib/enterprise.xml"
|
87
|
+
HooplaSalesforce.metadata_wsdl = "lib/metadata.xml"
|
88
|
+
|
89
|
+
namespace :deploy do
|
90
|
+
HooplaSalesforce::Rake::DeployTask.new(:development) do |t|
|
91
|
+
t.username = "you@development.org"
|
92
|
+
t.password = "yourpassword"
|
93
|
+
t.token = "your security token"
|
94
|
+
t.namespace = ""
|
95
|
+
end
|
96
|
+
end
|
97
|
+
EOS
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def make_gitignore
|
102
|
+
File.open("#{directory}/.gitignore", 'w') do |gitignore|
|
103
|
+
gitignore.print <<-EOS.margin
|
104
|
+
src-processed
|
105
|
+
deploy.zip
|
106
|
+
EOS
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def followup_instructions
|
111
|
+
puts
|
112
|
+
puts "-" * 80
|
113
|
+
puts " All done. Now you need to download your WSDL files into your project:"
|
114
|
+
puts
|
115
|
+
puts " Enterprise WSDL: #{directory}/lib/enterprise.xml"
|
116
|
+
puts " Metadata WSDL: #{directory}/lib/metadata.xml"
|
117
|
+
puts
|
118
|
+
puts "-" * 80
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'hoopla_salesforce/eruby'
|
2
|
+
|
3
|
+
module HooplaSalesforce
|
4
|
+
class TemplateProcessor
|
5
|
+
class Base
|
6
|
+
include HooplaSalesforce::CaptureHelper
|
7
|
+
|
8
|
+
attr_reader :src, :base, :file
|
9
|
+
def initialize(src, file)
|
10
|
+
@src = src
|
11
|
+
@base = File.basename(file, '.erb')
|
12
|
+
@file = file
|
13
|
+
template = Eruby.new(File.read(file))
|
14
|
+
|
15
|
+
File.open(output_file, 'w') do |f|
|
16
|
+
f.print template.result(binding)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_resource_file(files, extension)
|
21
|
+
files.map do |file|
|
22
|
+
file += ".#{extension}" unless extension =~ /\.#{extension}$/
|
23
|
+
yield file
|
24
|
+
end.join("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Generic < Base
|
29
|
+
def output_file
|
30
|
+
file.sub(/\.erb$/, '')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class VisualForce < Base
|
35
|
+
def page(opts={})
|
36
|
+
params = opts.map { |key, val| %Q|#{key}="#{val}"| }.join(" ")
|
37
|
+
"<apex:page #{params}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
def end_page
|
41
|
+
"</apex:page>"
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_url(file)
|
45
|
+
resource, file = file.split('/', 2)
|
46
|
+
"{!URLFOR($Resource.#{resource}, '/#{file}')}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def stylesheet_include_tag(*files)
|
50
|
+
each_resource_file(files, "css") do |file|
|
51
|
+
%Q|<apex:stylesheet value="#{resource_url(file)}" />|
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def javascript_include_tag(*files)
|
56
|
+
each_resource_file(files, "js") do |file|
|
57
|
+
%Q|<script type="text/javascript" src="#{resource_url(file)}"></script>|
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def as_json_array(collection, var)
|
62
|
+
<<-EOS.margin
|
63
|
+
[<apex:repeat value="{!#{collection}}" var="#{var}" rows="1">
|
64
|
+
#{send("#{var}_json")}
|
65
|
+
</apex:repeat>
|
66
|
+
<apex:repeat value="{!#{collection}}" var="#{var}" first="1">
|
67
|
+
,#{send("#{var}_json")}
|
68
|
+
</apex:repeat>]
|
69
|
+
EOS
|
70
|
+
end
|
71
|
+
|
72
|
+
def output_file
|
73
|
+
"#{src}/pages/#{base}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class TestPage < Base
|
78
|
+
def page(opts={})
|
79
|
+
<<-EOS.margin
|
80
|
+
<html>
|
81
|
+
<head>
|
82
|
+
<title>Test Page: #{opts[:controller]}</title>
|
83
|
+
</head>
|
84
|
+
<body>
|
85
|
+
EOS
|
86
|
+
end
|
87
|
+
|
88
|
+
def end_page
|
89
|
+
<<-EOS.margin
|
90
|
+
</body>
|
91
|
+
</html>
|
92
|
+
EOS
|
93
|
+
end
|
94
|
+
|
95
|
+
def resource_url(file)
|
96
|
+
"../resources/#{file}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def stylesheet_include_tag(*files)
|
100
|
+
each_resource_file(files, "css") do |file|
|
101
|
+
%Q|<link rel="stylesheet" type="text/css" href="#{resource_url(file)}" />|
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def javascript_include_tag(*files)
|
106
|
+
each_resource_file(files, "js") do |file|
|
107
|
+
%Q|<script type="text/javascript" src="#{resource_url(file)}"></script>|
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def as_json_array(collection, var)
|
112
|
+
send("#{var}_json")
|
113
|
+
end
|
114
|
+
|
115
|
+
def output_file
|
116
|
+
"#{src}/pages-test/#{base}.html"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -33,7 +33,7 @@ module HooplaSalesforce
|
|
33
33
|
test_failures = []
|
34
34
|
coverage_warnings = []
|
35
35
|
|
36
|
-
result[:messages].each do |message|
|
36
|
+
sanitize(result[:messages]).each do |message|
|
37
37
|
if message[:success]
|
38
38
|
status = " "
|
39
39
|
status = "U" if message[:changed]
|
@@ -65,6 +65,7 @@ module HooplaSalesforce
|
|
65
65
|
test_result = result[:run_test_result]
|
66
66
|
failures = test_result[:num_failures].to_i
|
67
67
|
passes = test_result[:num_tests_run].to_i - failures
|
68
|
+
passes = 0 if passes < 0
|
68
69
|
print "#{indent}Passes: #{green}#{passes}#{end_color} "
|
69
70
|
print "Failures: #{red}#{failures}#{end_color} "
|
70
71
|
puts "Duration: #{test_result[:total_time]}"
|
@@ -73,7 +74,7 @@ module HooplaSalesforce
|
|
73
74
|
# :failures is only an array if we have more than 1. Fun...
|
74
75
|
sanitize(test_result[:failures]).each do |failure|
|
75
76
|
message = "#{indent}#{red}#{failure[:name]}.#{failure[:method_name]}: #{failure[:message]}\n"
|
76
|
-
message += failure[:stack_trace].split("\n").map{ |l| indent * 2 + l }.join("\n")
|
77
|
+
message += failure[:stack_trace].split("\n").map{ |l| indent * 2 + l }.join("\n") if failure[:stack_trace]
|
77
78
|
message += end_color
|
78
79
|
test_failures << message
|
79
80
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module HooplaSalesforce
|
5
|
+
class WebAgent
|
6
|
+
LOGIN_URL = 'https://login.salesforce.com'
|
7
|
+
MAX_CLASSES = 10_000
|
8
|
+
|
9
|
+
attr_reader :username, :password, :agent
|
10
|
+
|
11
|
+
def initialize(username, password)
|
12
|
+
@username = username
|
13
|
+
@password = password
|
14
|
+
@agent = Mechanize.new
|
15
|
+
login
|
16
|
+
end
|
17
|
+
|
18
|
+
def login
|
19
|
+
agent.get(LOGIN_URL).form_with(:name => 'login') do |login|
|
20
|
+
login.un = username
|
21
|
+
login.pw = password
|
22
|
+
end.submit
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_run_url(test, namespace)
|
26
|
+
"/setup/build/runApexTest.apexp?class_id=#{test[:id]}&class_name=#{test[:name]}&ns_prefix=#{namespace}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_results_dir
|
30
|
+
"test-results"
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_results_file(test_name)
|
34
|
+
"#{test_results_dir}/#{test_name}.html"
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_all_test_links
|
38
|
+
page = agent.get('/01p')
|
39
|
+
|
40
|
+
if page.search('.next')
|
41
|
+
expand_link = page.search('.fewerMore a').first['href']
|
42
|
+
enough_rows = expand_link.sub(/rowsperpage=\d+/, "rowsperpage=#{MAX_CLASSES}")
|
43
|
+
page = agent.get(enough_rows)
|
44
|
+
end
|
45
|
+
|
46
|
+
page.search('.dataCell[scope="row"] a')
|
47
|
+
end
|
48
|
+
|
49
|
+
# Runs the given tests from the Web UI
|
50
|
+
# namespace is the package namespace without the __
|
51
|
+
def run_tests(test_names, namespace = '')
|
52
|
+
test_links = get_all_test_links
|
53
|
+
|
54
|
+
tests = test_names.map do |test_name|
|
55
|
+
{ :id => test_links.detect{ |a| a.text == test_name }['href'][1..-1],
|
56
|
+
:name => test_name }
|
57
|
+
end
|
58
|
+
|
59
|
+
mkdir_p test_results_dir
|
60
|
+
tests.each do |test|
|
61
|
+
results_page = agent.get(test_run_url(test, namespace))
|
62
|
+
results_table = results_page.search('.outer table td').first
|
63
|
+
File.open(test_results_file(test[:name]), 'w') do |f|
|
64
|
+
f.print results_table.inner_html
|
65
|
+
end
|
66
|
+
|
67
|
+
puts "#{test[:name]} complete, results in #{test_results_file(test[:name])}."
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Trotter Cashion
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-08-
|
18
|
+
date: 2010-08-27 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -46,21 +46,74 @@ dependencies:
|
|
46
46
|
version: 0.9.4
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
|
-
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: commander
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 4
|
58
|
+
- 0
|
59
|
+
- 3
|
60
|
+
version: 4.0.3
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: erubis
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 2
|
72
|
+
- 6
|
73
|
+
- 6
|
74
|
+
version: 2.6.6
|
75
|
+
type: :runtime
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: mechanize
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 1
|
86
|
+
- 0
|
87
|
+
- 0
|
88
|
+
version: 1.0.0
|
89
|
+
type: :runtime
|
90
|
+
version_requirements: *id005
|
91
|
+
description: Helpers for building Salesforce.com projects
|
50
92
|
email: dev@hoopla.net
|
51
|
-
executables:
|
52
|
-
|
93
|
+
executables:
|
94
|
+
- hoopla_salesforce
|
53
95
|
extensions: []
|
54
96
|
|
55
97
|
extra_rdoc_files: []
|
56
98
|
|
57
99
|
files:
|
100
|
+
- README.md
|
101
|
+
- bin/hoopla_salesforce
|
58
102
|
- lib/hoopla_salesforce/rake.rb
|
59
103
|
- lib/hoopla_salesforce/rake/base_task.rb
|
60
104
|
- lib/hoopla_salesforce/rake/deploy_task.rb
|
61
105
|
- lib/hoopla_salesforce/rake/retrieve_task.rb
|
62
|
-
- lib/hoopla_salesforce/
|
106
|
+
- lib/hoopla_salesforce/eruby.rb
|
107
|
+
- lib/hoopla_salesforce/web_agent.rb
|
108
|
+
- lib/hoopla_salesforce/utils.rb
|
63
109
|
- lib/hoopla_salesforce/text_reporter.rb
|
110
|
+
- lib/hoopla_salesforce/skeleton.rb
|
111
|
+
- lib/hoopla_salesforce/template_processor.rb
|
112
|
+
- lib/hoopla_salesforce/package_generator.rb
|
113
|
+
- lib/hoopla_salesforce/commands.rb
|
114
|
+
- lib/hoopla_salesforce/info.rb
|
115
|
+
- lib/hoopla_salesforce/ext/string.rb
|
116
|
+
- lib/hoopla_salesforce/ext/commander.rb
|
64
117
|
- lib/hoopla_salesforce/deployer.rb
|
65
118
|
- lib/hoopla_salesforce.rb
|
66
119
|
has_rdoc: true
|
@@ -92,6 +145,6 @@ rubyforge_project: nowarning
|
|
92
145
|
rubygems_version: 1.3.6
|
93
146
|
signing_key:
|
94
147
|
specification_version: 3
|
95
|
-
summary:
|
148
|
+
summary: Helpers for building Salesforce.com projects
|
96
149
|
test_files: []
|
97
150
|
|