hudkins 0.0.1
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/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +29 -0
- data/README.txt +98 -0
- data/Rakefile +25 -0
- data/bin/hudkins +5 -0
- data/lib/assets/config.xml +30 -0
- data/lib/assets/config_snippets/builders.xml +5 -0
- data/lib/assets/free_style_project.xml.erb +14 -0
- data/lib/assets/hudkinsrc +4 -0
- data/lib/hudkins.rb +263 -0
- data/lib/hudkins/command.rb +127 -0
- data/lib/hudkins/command/exec.rb +113 -0
- data/lib/hudkins/command/irb_start.rb +86 -0
- data/lib/hudkins/common.rb +69 -0
- data/lib/hudkins/errors.rb +7 -0
- data/lib/hudkins/job.rb +168 -0
- data/lib/hudkins/jobs.rb +86 -0
- data/lib/hudkins/mixin.rb +103 -0
- data/lib/hudkins/rake.rb +20 -0
- data/lib/hudkins/restclient.rb +130 -0
- data/lib/hudkins/sysinfo.rb +11 -0
- data/test/fixtures/config.erb +30 -0
- data/test/fixtures/jobs.erb +22 -0
- data/test/fixtures/new_project_config.erb +14 -0
- data/test/test_helper.rb +50 -0
- data/test/unit/hudkins/test_job.rb +65 -0
- data/test/unit/hudkins/test_jobs.rb +27 -0
- data/test/unit/test_hudkins.rb +74 -0
- metadata +153 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
require "hudkins"
|
2
|
+
require "optparse"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
##
|
6
|
+
# === Description
|
7
|
+
# Main class for the included bin to interact with Hudson at the command line.
|
8
|
+
#
|
9
|
+
# The Commands are setup in Hudkins::Command::Exec
|
10
|
+
#
|
11
|
+
# === Usage
|
12
|
+
# see `hudkins -h'
|
13
|
+
#
|
14
|
+
# a host option is required. Can be configured in env variable +hudkins_host+ or
|
15
|
+
# config file. all options can be specified in a yaml file in +~/.hudkinsrc+
|
16
|
+
# and/or +`pwd`/.hudkinsrc+
|
17
|
+
class Hudkins::Command
|
18
|
+
include Hudkins::Common
|
19
|
+
|
20
|
+
require "hudkins/command/exec"
|
21
|
+
# all the run_ commands
|
22
|
+
extend Hudkins::Command::Exec
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def run
|
26
|
+
parse_options
|
27
|
+
begin
|
28
|
+
return_string = send( @command )
|
29
|
+
puts return_string if return_string
|
30
|
+
rescue => e
|
31
|
+
raise_usage e.message
|
32
|
+
end
|
33
|
+
if @options[:irb]
|
34
|
+
run_start_irb
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# commands:
|
39
|
+
# helpers:
|
40
|
+
def usage_msg
|
41
|
+
usage = <<-EOB
|
42
|
+
|
43
|
+
Usage: hudkins [opts] commands [job_name]
|
44
|
+
|
45
|
+
Commands: unambiguous_partial_command
|
46
|
+
build: start a job building.
|
47
|
+
config: pretty print job config. job_name
|
48
|
+
create: create new hudson job.
|
49
|
+
host: print hudson host url
|
50
|
+
list: list jobs. [job_name used as filter].
|
51
|
+
|
52
|
+
|
53
|
+
Optional: job_name (depends on command)
|
54
|
+
may also be partial (picks first match)
|
55
|
+
|
56
|
+
EOB
|
57
|
+
end
|
58
|
+
|
59
|
+
def raise_usage msg = nil
|
60
|
+
warn msg if msg
|
61
|
+
puts usage_msg
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_options
|
66
|
+
# initialize cmd_list. This seems hacky, but I like having the cmd_list
|
67
|
+
# near the usage statement.
|
68
|
+
@options = {}
|
69
|
+
|
70
|
+
OptionParser.new do |opts|
|
71
|
+
opts.banner = usage_msg
|
72
|
+
|
73
|
+
opts.separator ""
|
74
|
+
opts.separator "Specific options:"
|
75
|
+
|
76
|
+
opts.on("--hud-host HOST", "Hudson host to connect to. Overrides rc file") do |h|
|
77
|
+
@options[:host] = h
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on("-i", "--irb", "Drop into irb.") do |i|
|
81
|
+
@options[:irb] = i
|
82
|
+
end
|
83
|
+
opts.on("-v", "--[no-]verbose", "Turn on verbose messages.") do |v|
|
84
|
+
$VERBOSE = @options[:verbose] = v
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.separator ""
|
88
|
+
opts.separator "Common options:"
|
89
|
+
|
90
|
+
opts.on_tail("--version", "Show version") do
|
91
|
+
puts "Hudkins (#{Hudkins::VERSION}) (c) 2010"
|
92
|
+
exit
|
93
|
+
end
|
94
|
+
|
95
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
96
|
+
puts opts
|
97
|
+
exit
|
98
|
+
end
|
99
|
+
|
100
|
+
end.parse!
|
101
|
+
|
102
|
+
config.merge! @options
|
103
|
+
|
104
|
+
@command, @job_name = ARGV.shift(2)
|
105
|
+
@command ||= "default"
|
106
|
+
|
107
|
+
@hud = Hudkins.new hud_host
|
108
|
+
job @job_name
|
109
|
+
|
110
|
+
parse_command
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_command
|
114
|
+
# select unambiguous command to run.
|
115
|
+
cmds = cmd_list.select {|c| Regexp.new(@command.to_s, Regexp::IGNORECASE) === c}
|
116
|
+
case cmds.size
|
117
|
+
when 0 then
|
118
|
+
raise_usage "#{@command} is not recognized as a valid command."
|
119
|
+
when 1 then
|
120
|
+
@command = "run_" << cmds.first
|
121
|
+
else
|
122
|
+
raise_usage "Ambiguous command. Please specify:\n #{cmds.join("\n ")}\n\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class Hudkins
|
2
|
+
##
|
3
|
+
# === Description
|
4
|
+
# mixin for interacting with the lib
|
5
|
+
#
|
6
|
+
# the bin is setup to run any command prefaced with `run_'
|
7
|
+
# each command should return something to be puts or nil
|
8
|
+
module Command::Exec
|
9
|
+
##
|
10
|
+
# default command. here as a place holder
|
11
|
+
def run_default
|
12
|
+
end
|
13
|
+
|
14
|
+
def run_build
|
15
|
+
"building!!!"
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_config job_name = @job_name
|
19
|
+
required_params job_name => "job_name is required for this command."
|
20
|
+
[
|
21
|
+
job( job_name ).name,
|
22
|
+
job( job_name ).config
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_create job_name = @job_name
|
27
|
+
required_params job_name => "job_name is required for this command."
|
28
|
+
"creating!!!"
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_host
|
32
|
+
hud_host
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_list job_name = @job_name
|
36
|
+
names( job_name || "" )
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_start_irb
|
40
|
+
puts <<-EOS
|
41
|
+
|
42
|
+
---
|
43
|
+
Welcome to hudkins irb console.
|
44
|
+
type hudkins for help.
|
45
|
+
|
46
|
+
EOS
|
47
|
+
require "hudkins/command/irb_start"
|
48
|
+
# turn job names into methods
|
49
|
+
extend Hudkins::Command::Irb
|
50
|
+
IRB.start_session(nil, binding)
|
51
|
+
end
|
52
|
+
|
53
|
+
# helper commands
|
54
|
+
|
55
|
+
def hud
|
56
|
+
@hud
|
57
|
+
end
|
58
|
+
|
59
|
+
def job job_name = nil
|
60
|
+
if job_name
|
61
|
+
@job = hud.jobs.find_by_name job_name
|
62
|
+
end
|
63
|
+
@job
|
64
|
+
end
|
65
|
+
|
66
|
+
# could probably be cleaned up
|
67
|
+
def required_params values
|
68
|
+
e = Hudkins::ArgumentError.new ""
|
69
|
+
raize = false
|
70
|
+
values.each do |k,msg|
|
71
|
+
e << msg unless k
|
72
|
+
raize = true unless k
|
73
|
+
end
|
74
|
+
raise e if raize
|
75
|
+
end
|
76
|
+
|
77
|
+
def cmd_list
|
78
|
+
command_list.map {|m| m.gsub(/^run_/, '')}
|
79
|
+
end
|
80
|
+
|
81
|
+
def command_list
|
82
|
+
self.methods.select {|m| m =~ /^run_/}
|
83
|
+
end
|
84
|
+
|
85
|
+
def names name = ""
|
86
|
+
hud.jobs.names name
|
87
|
+
end
|
88
|
+
|
89
|
+
def hud_host
|
90
|
+
ENV["hudkins_host"] || config[:host] || raise_usage( "no hudkins_host defined." )
|
91
|
+
end
|
92
|
+
|
93
|
+
def config
|
94
|
+
@config ||= load_rc
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_rc
|
98
|
+
hudkins_rc.inject({}) do |ret, rc|
|
99
|
+
h = YAML.load_file( rc )
|
100
|
+
ret.merge! h if h
|
101
|
+
ret
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def hudkins_rc
|
106
|
+
[
|
107
|
+
File.expand_path("~/.hudkinsrc"),
|
108
|
+
File.join(Dir.pwd, ".hudkinsrc")
|
109
|
+
].select {|f| File.size? f} # exists and is non 0
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "irb"
|
2
|
+
|
3
|
+
# thank you!
|
4
|
+
# http://jameskilton.com/2009/04/02/embedding-irb-into-your-ruby-application/
|
5
|
+
|
6
|
+
module IRB # :nodoc:
|
7
|
+
# originally used the code snippet from the above link, which turned out to
|
8
|
+
# be a snippet from `ruby-debug'. However, I was having problems with CNTR-C
|
9
|
+
# and I realized I wanted to mimic irb, just be able to actually pass Irb.new
|
10
|
+
# a workspace param which it expected.
|
11
|
+
def self.start_session(ap_path = nil, binding = nil)
|
12
|
+
@@hack_binding = binding
|
13
|
+
IRB.start ap_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.get_binding
|
17
|
+
@@hack_binding
|
18
|
+
end
|
19
|
+
|
20
|
+
class Irb
|
21
|
+
alias_method :org_initialize, :initialize
|
22
|
+
def initialize *args
|
23
|
+
workspace = args.shift
|
24
|
+
workspace ||= WorkSpace.new(IRB.get_binding)
|
25
|
+
args.unshift workspace
|
26
|
+
org_initialize *args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Hudkins
|
32
|
+
module Command::Irb
|
33
|
+
|
34
|
+
##
|
35
|
+
# In the irb console I want to access the same commands as
|
36
|
+
# Hudkins::Command::Exec but without the `run_' part.
|
37
|
+
def self.extend_object obj # :doc:
|
38
|
+
# in IRB allow run_* commands to be exec without `run_'
|
39
|
+
obj.command_list.each do |cmd|
|
40
|
+
s = <<-EOE
|
41
|
+
def self.#{cmd.gsub(/^run_/, '')} *args; #{cmd} *args; end
|
42
|
+
EOE
|
43
|
+
obj.module_eval s
|
44
|
+
end
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# This is cool. Override default method to add convenience methods for each
|
50
|
+
# job_name in #jobs. This allows `job_name' to be used as a method to
|
51
|
+
# access that particular job.
|
52
|
+
def method_missing sym, *args, &block
|
53
|
+
if job = @hud.jobs.find_by_name( sym )
|
54
|
+
job
|
55
|
+
else
|
56
|
+
super sym, *args, &block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# help method in irb console
|
62
|
+
def hudkins
|
63
|
+
puts <<-EOS
|
64
|
+
|
65
|
+
self is Hudkins::Command
|
66
|
+
hudkins => This help message
|
67
|
+
reload! => reload lib
|
68
|
+
hud => Hudkins.new <hud_host>
|
69
|
+
job [job_name] => Hudkins::Job
|
70
|
+
job_name => Hudkins::Job
|
71
|
+
commands:
|
72
|
+
#{cmd_list.join("\n ")}
|
73
|
+
|
74
|
+
EOS
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# helper method in irb console to reload all the lib files.
|
79
|
+
def reload!
|
80
|
+
$".grep( /hudkins/ ).each do |f|
|
81
|
+
load f
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end # Command::Irb
|
86
|
+
end # Hudkins
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Hudkins
|
2
|
+
# common methods available for all classes
|
3
|
+
module Common
|
4
|
+
|
5
|
+
# hide instance variables
|
6
|
+
def inspect( string = nil )
|
7
|
+
new_string = " " << string if string
|
8
|
+
"#<%s:0x%x%s>" % [self.class, (self.object_id << 1), new_string]
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# === Description
|
13
|
+
# useful for escaping parameters to be sent to #get/#post
|
14
|
+
#
|
15
|
+
# === Parameters
|
16
|
+
# +Hash+:: converts a simple one dimentional hash to a url parameter
|
17
|
+
# query string and uri escapes it.
|
18
|
+
# +else+:: converts to string and uri escapes it.
|
19
|
+
def url_escape msg
|
20
|
+
case msg
|
21
|
+
when Hash then
|
22
|
+
URI.escape( msg.map {|k,v| "#{k}=#{v}" }.join("&") )
|
23
|
+
else
|
24
|
+
URI.escape( msg.to_s )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#--
|
29
|
+
# these classes are being included in the @hudkins class.
|
30
|
+
#++
|
31
|
+
|
32
|
+
def hudkins
|
33
|
+
@hudkins
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# accessor methods for child classes to get
|
38
|
+
def get *args
|
39
|
+
@hudkins.get *args
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# accessor methods for child classes to post
|
44
|
+
def post *args
|
45
|
+
@hudkins.post *args
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# TODO host is not universally accessible so this method might not belong here..
|
50
|
+
# === Notes
|
51
|
+
# My Hudson server is not publicially accessible so when I can't connect,
|
52
|
+
# Socket#getaddrinfo (RestClient) times out after more than 20 seconds! I
|
53
|
+
# created a healper class Hudkins::HostLookup to timeout Socket#getaddrinfo
|
54
|
+
# after Hudkins::HostLookup#TIMEOUT seconds (defaults to 2).
|
55
|
+
def host_available?
|
56
|
+
HostLookup.available? @host, @options[:host_timeout]
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# TODO host is not universally accessible so this method might not belong here..
|
61
|
+
# Raise TimeoutError unless hudson host is responding within specified number of seconds.
|
62
|
+
def check_host_availability
|
63
|
+
# host is URI.
|
64
|
+
msg = "`%s' did not respond within the set number of seconds." % [ host.host ]
|
65
|
+
raise TimeoutError, msg unless host_available?
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/hudkins/job.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# === Description
|
2
|
+
# Primary class for interacting with a Hudson job
|
3
|
+
#
|
4
|
+
# === Examples
|
5
|
+
# hud = Hudkins.new "http://hudson.com"
|
6
|
+
# job = hud.jobs.find_by_name :job_name
|
7
|
+
#
|
8
|
+
# job.disabled? # => true
|
9
|
+
# job.disabled = false
|
10
|
+
# job.post_config!
|
11
|
+
#
|
12
|
+
# === attr_accessor_from_config methods
|
13
|
+
#
|
14
|
+
# I created custom attr_accessor like DSL methods that create accessor like
|
15
|
+
# methods for the Hudkins::Job object to easily interact with the xml config.
|
16
|
+
# Paradigm is to use method_name? for boolean values and method_name! for any
|
17
|
+
# methods that post updates to the server.
|
18
|
+
#
|
19
|
+
class Hudkins::Job
|
20
|
+
include Hudkins::Common
|
21
|
+
extend Hudkins::Mixin
|
22
|
+
|
23
|
+
include Comparable
|
24
|
+
|
25
|
+
attr_reader :name, :url, :color, :path, :config
|
26
|
+
|
27
|
+
#TODO figure out how to define a custom rdoc method to correctly document these
|
28
|
+
|
29
|
+
##
|
30
|
+
# :attr_accessor: scm_url
|
31
|
+
attr_accessor_from_config :scm_url, "//scm//remote"
|
32
|
+
##
|
33
|
+
# :attr_accessor: description
|
34
|
+
attr_accessor_from_config :description, "//project//descriptoin"
|
35
|
+
##
|
36
|
+
# :attr_accessor: can_roam
|
37
|
+
attr_accessor_from_config :can_roam, "//project//canRoam", :bool
|
38
|
+
##
|
39
|
+
# :attr_accessor: disabled
|
40
|
+
attr_accessor_from_config :disabled, "//project//disabled", :bool
|
41
|
+
##
|
42
|
+
# :attr_accessor: blocked_by_upstream
|
43
|
+
attr_accessor_from_config :blocked_by_upstream, "//project//blockBuildWhenUpstreamBuilding", :bool
|
44
|
+
##
|
45
|
+
# :attr_accessor: concurrent_builds
|
46
|
+
attr_accessor_from_config :concurrent_builds, "//project//concurrentBuild", :bool
|
47
|
+
attr_accessor_from_config :mail_recipients, "//publishers//recipients"
|
48
|
+
# this is really an array. I haven't added functionality for that yet
|
49
|
+
attr_accessor_from_config :shell_builders, "//builders/hudson.tasks.Shell", :array
|
50
|
+
##
|
51
|
+
# :attr_accessor: :rotate_logs_num
|
52
|
+
# Number of builds to keep
|
53
|
+
# -1 == infinite
|
54
|
+
attr_accessor_from_config :rotate_logs_num, "//logRotator/numToKeep", :int
|
55
|
+
##
|
56
|
+
# :attr_accessor: :rotate_logs_days
|
57
|
+
# Number of days to keep builds
|
58
|
+
# -1 == infinite
|
59
|
+
attr_accessor_from_config :rotate_logs_days, "//logRotator/daysToKeep", Integer
|
60
|
+
|
61
|
+
def initialize hudkins, data
|
62
|
+
@hudkins = hudkins
|
63
|
+
@name = data["name"]
|
64
|
+
@url = URI.parse data["url"]
|
65
|
+
@color = data["color"]
|
66
|
+
@path = @url.path
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
super "@name=#{@name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def url
|
74
|
+
@url.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# Enumerables/Comparables...
|
78
|
+
def <=> other
|
79
|
+
# TODO how do I implement jobs.sort(&:path) ?
|
80
|
+
self.name <=> other.name
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# === Description
|
85
|
+
# get the job's config
|
86
|
+
# takes string (xml) or other's config
|
87
|
+
def update_config config = nil
|
88
|
+
config = case config
|
89
|
+
when String then
|
90
|
+
hudkins.parse_string( config, :xml )
|
91
|
+
when Nokogiri::XML::Document, NilClass then
|
92
|
+
config
|
93
|
+
else
|
94
|
+
raise "unknown config type #{config.class}"
|
95
|
+
end
|
96
|
+
@config = config || hudkins.get_parsed( path + "/config.xml", :accept => "application/xml" )
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# === Description
|
101
|
+
# accessor for job's config. Initializes then caches. Use update_config if out of date.
|
102
|
+
def config
|
103
|
+
@config ||= update_config
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# === Description
|
108
|
+
# Post the job's config back to the server to update it.
|
109
|
+
def post_config!
|
110
|
+
post path + "/config.xml", @config
|
111
|
+
end
|
112
|
+
|
113
|
+
def build!
|
114
|
+
get path + "/build"
|
115
|
+
end
|
116
|
+
|
117
|
+
def delete!
|
118
|
+
post path + "/doDelete"
|
119
|
+
end
|
120
|
+
|
121
|
+
def recreate!
|
122
|
+
hudkins.add_job name, config
|
123
|
+
end
|
124
|
+
|
125
|
+
def disable!
|
126
|
+
post path + "/disable"
|
127
|
+
end
|
128
|
+
|
129
|
+
def enable!
|
130
|
+
post path + "/enable"
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# === Description
|
135
|
+
# The remote api allows for updating just the description. I had to tweak the
|
136
|
+
# name because I still wanted description to be an attr_accessor_from_config
|
137
|
+
#
|
138
|
+
# === Example
|
139
|
+
# job.quick_description! # => "this is the description for job"
|
140
|
+
# job.quick_description! = "this is the new desc." # => Response obj
|
141
|
+
def quick_description! msg = nil
|
142
|
+
# another yuck!
|
143
|
+
if msg
|
144
|
+
post path + "/description?" + url_escape(:description => msg)
|
145
|
+
else
|
146
|
+
get( path + "/description" ).body
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# === Description
|
152
|
+
# Copy job to new_job_name
|
153
|
+
#
|
154
|
+
# === Example
|
155
|
+
# new_job = job.copy "new-job-name"
|
156
|
+
# new_job.scm_url = "http://svn/new/job/path"
|
157
|
+
# new_job.post_config!
|
158
|
+
def copy new_job_name
|
159
|
+
# post /createItem?name=NEWJOBNAME&mode=copy&from=FROMJOBNAME
|
160
|
+
response = post "/createItem?" +
|
161
|
+
url_escape(:name => new_job_name, :mode => "copy", :from => name)
|
162
|
+
# return new job object
|
163
|
+
hudkins.update_jobs.find_by_name new_job_name if response.success?
|
164
|
+
end
|
165
|
+
|
166
|
+
# not sure if we want this.
|
167
|
+
# alias_method :dup, :copy
|
168
|
+
end
|