xat 1.32.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.
- checksums.yaml +7 -0
- data/LICENSE +176 -0
- data/README.md +38 -0
- data/app_template/README.md +14 -0
- data/app_template/app.css +0 -0
- data/app_template/app.js +12 -0
- data/app_template/assets/banner.png +0 -0
- data/app_template/assets/logo-promotion.png +0 -0
- data/app_template/assets/logo-small.png +0 -0
- data/app_template/assets/logo.png +0 -0
- data/app_template/manifest.json.tt +13 -0
- data/app_template/templates/layout.hdbs +10 -0
- data/app_template/translations/en.json +16 -0
- data/app_template_iframe/README.md +14 -0
- data/app_template_iframe/assets/banner.png +0 -0
- data/app_template_iframe/assets/iframe.html +13 -0
- data/app_template_iframe/assets/logo-promotion.png +0 -0
- data/app_template_iframe/assets/logo-small.png +0 -0
- data/app_template_iframe/assets/logo.png +0 -0
- data/app_template_iframe/manifest.json.tt +15 -0
- data/app_template_iframe/translations/en.json +16 -0
- data/bin/xat +5 -0
- data/features/clean.feature +9 -0
- data/features/fixtures/quote_character_translation.json +6 -0
- data/features/new.feature +117 -0
- data/features/package.feature +22 -0
- data/features/step_definitions/app_steps.rb +103 -0
- data/features/support/env.rb +5 -0
- data/features/validate.feature +15 -0
- data/lib/xat.rb +5 -0
- data/lib/zendesk_apps_tools/api_connection.rb +33 -0
- data/lib/zendesk_apps_tools/array_patch.rb +22 -0
- data/lib/zendesk_apps_tools/bump.rb +60 -0
- data/lib/zendesk_apps_tools/cache.rb +25 -0
- data/lib/zendesk_apps_tools/command.rb +183 -0
- data/lib/zendesk_apps_tools/command_helpers.rb +20 -0
- data/lib/zendesk_apps_tools/common.rb +40 -0
- data/lib/zendesk_apps_tools/deploy.rb +94 -0
- data/lib/zendesk_apps_tools/directory.rb +28 -0
- data/lib/zendesk_apps_tools/locale_identifier.rb +10 -0
- data/lib/zendesk_apps_tools/manifest_handler.rb +72 -0
- data/lib/zendesk_apps_tools/package_helper.rb +30 -0
- data/lib/zendesk_apps_tools/server.rb +65 -0
- data/lib/zendesk_apps_tools/settings.rb +82 -0
- data/lib/zendesk_apps_tools/translate.rb +168 -0
- data/templates/translation.erb.tt +13 -0
- metadata +238 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
Feature: package a zendesk app into a zip file
|
2
|
+
|
3
|
+
Background: create a new zendesk app
|
4
|
+
Given an app is created in directory "tmp/aruba"
|
5
|
+
|
6
|
+
Scenario: package a zendesk app by running 'xat package' command
|
7
|
+
When I run the command "xat package --path tmp/aruba" to package the app
|
8
|
+
And the command output should contain "adding app.js"
|
9
|
+
And the command output should contain "adding assets/logo-small.png"
|
10
|
+
And the command output should contain "adding assets/logo.png"
|
11
|
+
And the command output should contain "adding manifest.json"
|
12
|
+
And the command output should contain "adding templates/layout.hdbs"
|
13
|
+
And the command output should contain "adding translations/en.json"
|
14
|
+
And the command output should contain "created"
|
15
|
+
And the zip file should exist in directory "tmp/aruba/tmp"
|
16
|
+
|
17
|
+
|
18
|
+
Scenario: package a zendesk app by running 'xat package' command
|
19
|
+
When I create a symlink from "./templates/translation.erb.tt" to "tmp/aruba/assets/translation.erb.tt"
|
20
|
+
Then "tmp/aruba/assets/translation.erb.tt" should be a symlink
|
21
|
+
When I run the command "xat package --path tmp/aruba" to package the app
|
22
|
+
Then the zip file in "tmp/aruba/tmp" should not contain any symlinks
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'zip/zip'
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
When /^I move to the app directory$/ do
|
6
|
+
@previous_dir = Dir.pwd
|
7
|
+
Dir.chdir(@app_dir)
|
8
|
+
end
|
9
|
+
|
10
|
+
When /^I reset the working directory$/ do
|
11
|
+
Dir.chdir(@previous_dir)
|
12
|
+
end
|
13
|
+
|
14
|
+
Given /^an app directory "(.*?)" exists$/ do |app_dir|
|
15
|
+
@app_dir = app_dir
|
16
|
+
FileUtils.rm_rf(@app_dir)
|
17
|
+
FileUtils.mkdir_p(@app_dir)
|
18
|
+
end
|
19
|
+
|
20
|
+
Given /^an app is created in directory "(.*?)"$/ do |app_dir|
|
21
|
+
steps %(
|
22
|
+
Given an app directory "#{app_dir}" exists
|
23
|
+
And I run "xat new" command with the following details:
|
24
|
+
| author name | John Citizen |
|
25
|
+
| author email | john@example.com |
|
26
|
+
| author url | http://myapp.com |
|
27
|
+
| app name | John Test App |
|
28
|
+
| app dir | #{app_dir} |
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
When /^I run "(.*?)" command with the following details:$/ do |cmd, table|
|
33
|
+
IO.popen(cmd, 'w+') do |pipe|
|
34
|
+
# [ ['parameter name', 'value'] ]
|
35
|
+
table.raw.each do |row|
|
36
|
+
pipe.puts row.last
|
37
|
+
end
|
38
|
+
pipe.close_write
|
39
|
+
@output = pipe.readlines
|
40
|
+
@output.each { |line| puts line }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
When /^I create a symlink from "(.*?)" to "(.*?)"$/ do |src, dest|
|
45
|
+
@link_destname = File.basename(dest)
|
46
|
+
# create a symlink
|
47
|
+
FileUtils.ln_s(src, dest)
|
48
|
+
end
|
49
|
+
|
50
|
+
When /^I run the command "(.*?)" to (validate|package|clean) the app$/ do |cmd, _action|
|
51
|
+
IO.popen(cmd, 'w+') do |pipe|
|
52
|
+
pipe.puts "\n"
|
53
|
+
pipe.close_write
|
54
|
+
@output = pipe.readlines
|
55
|
+
@output.each { |line| puts line }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Then /^the app file "(.*?)" is created with:$/ do |file, content|
|
60
|
+
File.read(file).chomp.gsub(' ', '').should == content.gsub(' ', '')
|
61
|
+
end
|
62
|
+
|
63
|
+
Then /^the app file "(.*?)" is created$/ do |filename|
|
64
|
+
File.exist?(filename).should be_truthy
|
65
|
+
end
|
66
|
+
|
67
|
+
Then /^the fixture "(.*?)" is used for "(.*?)"$/ do |fixture, app_file|
|
68
|
+
fixture_file = File.join('features', 'fixtures', fixture)
|
69
|
+
app_file_path = File.join(@app_dir, app_file)
|
70
|
+
|
71
|
+
FileUtils.cp(fixture_file, app_file_path)
|
72
|
+
end
|
73
|
+
|
74
|
+
Then /^the zip file should exist in directory "(.*?)"$/ do |path|
|
75
|
+
Dir[path + '/app-*.zip'].size.should == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
Given /^I remove file "(.*?)"$/ do |file|
|
79
|
+
File.delete(file)
|
80
|
+
end
|
81
|
+
|
82
|
+
Then /^the zip file in "(.*?)" folder should not exist$/ do |path|
|
83
|
+
expect(Dir[path + '/app-*.zip'].size).to eq 0
|
84
|
+
end
|
85
|
+
|
86
|
+
Then /^it should pass the validation$/ do
|
87
|
+
@output.last.should =~ /OK/
|
88
|
+
$CHILD_STATUS.should == 0
|
89
|
+
end
|
90
|
+
|
91
|
+
Then /^the command output should contain "(.*?)"$/ do |output|
|
92
|
+
@output.join.should =~ /#{output}/
|
93
|
+
end
|
94
|
+
|
95
|
+
Then /^"(.*?)" should be a symlink$/ do |path|
|
96
|
+
File.symlink?(path).should be_truthy
|
97
|
+
end
|
98
|
+
|
99
|
+
Then /^the zip file in "(.*?)" should not contain any symlinks$/ do |path|
|
100
|
+
Zip::ZipFile.foreach Dir[path + '/app-*.zip'][0] do |p|
|
101
|
+
p.symlink?.should be_falsy
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Feature: validate a zendesk app
|
2
|
+
|
3
|
+
Validate a zendesk app by running 'xat package' command
|
4
|
+
|
5
|
+
Background: create a new zendesk app
|
6
|
+
Given an app is created in directory "tmp/aruba"
|
7
|
+
|
8
|
+
Scenario: valid app
|
9
|
+
When I run the command "xat validate --path tmp/aruba" to validate the app
|
10
|
+
Then it should pass the validation
|
11
|
+
|
12
|
+
Scenario: invalid app (missing manifest.json
|
13
|
+
Given I remove file "tmp/aruba/manifest.json"
|
14
|
+
When I run the command "xat validate --path tmp/aruba" to validate the app
|
15
|
+
Then the command output should contain "Could not find manifest.json"
|
data/lib/xat.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module APIConnection
|
3
|
+
FULL_URL = /https?:\/\//
|
4
|
+
URL_TEMPLATE = 'https://%s.zendesk.com/'
|
5
|
+
|
6
|
+
def prepare_api_auth
|
7
|
+
@subdomain ||= fetch_cache('subdomain') || get_value_from_stdin('Enter your Zendesk subdomain or full Zendesk URL:')
|
8
|
+
@username ||= fetch_cache('username') || get_value_from_stdin('Enter your username:')
|
9
|
+
@password ||= fetch_cache('password') || get_password_from_stdin('Enter your password:')
|
10
|
+
|
11
|
+
save_cache 'subdomain' => @subdomain, 'username' => @username
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_connection(encoding = :url_encoded)
|
15
|
+
prepare_api_auth
|
16
|
+
Faraday.new full_url do |f|
|
17
|
+
f.request encoding
|
18
|
+
f.adapter :net_http
|
19
|
+
f.basic_auth @username, @password
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def full_url
|
26
|
+
if FULL_URL =~ @subdomain
|
27
|
+
@subdomain
|
28
|
+
else
|
29
|
+
URL_TEMPLATE % @subdomain
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Array
|
2
|
+
unless instance_methods.include? :to_h
|
3
|
+
def to_h
|
4
|
+
if elem_index = index { |elem| !elem.is_a?(Array) }
|
5
|
+
raise TypeError.new("wrong element type #{self[elem_index].class} at #{elem_index} (expected array)")
|
6
|
+
end
|
7
|
+
|
8
|
+
each_with_index.inject({}) do |hash, elem|
|
9
|
+
pair, index = elem
|
10
|
+
|
11
|
+
if pair.size != 2
|
12
|
+
raise ArgumentError.new("wrong array length at #{index} (expected 2, was #{pair.size})")
|
13
|
+
end
|
14
|
+
|
15
|
+
hash.tap do |h|
|
16
|
+
key, val = pair
|
17
|
+
h[key] = val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'zendesk_apps_tools/manifest_handler'
|
5
|
+
|
6
|
+
module ZendeskAppsTools
|
7
|
+
class Bump < Thor
|
8
|
+
include Thor::Actions
|
9
|
+
prepend ManifestHandler
|
10
|
+
|
11
|
+
SHARED_OPTIONS = {
|
12
|
+
['commit', '-c'] => false,
|
13
|
+
['message', '-m'] => nil,
|
14
|
+
['tag', '-t'] => false
|
15
|
+
}
|
16
|
+
|
17
|
+
desc 'major', 'Bump major version'
|
18
|
+
method_options SHARED_OPTIONS
|
19
|
+
def major
|
20
|
+
semver[:major] += 1
|
21
|
+
semver[:minor] = 0
|
22
|
+
semver[:patch] = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'minor', 'Bump minor version'
|
26
|
+
method_options SHARED_OPTIONS
|
27
|
+
def minor
|
28
|
+
semver[:minor] += 1
|
29
|
+
semver[:patch] = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'patch', 'Bump patch version'
|
33
|
+
method_options SHARED_OPTIONS
|
34
|
+
def patch
|
35
|
+
semver[:patch] += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
default_task :patch
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def post_actions
|
43
|
+
return tag if options[:tag]
|
44
|
+
commit if options[:commit]
|
45
|
+
end
|
46
|
+
|
47
|
+
def commit
|
48
|
+
`git commit -am #{commit_message}`
|
49
|
+
end
|
50
|
+
|
51
|
+
def commit_message
|
52
|
+
options[:message] || version(v: true)
|
53
|
+
end
|
54
|
+
|
55
|
+
def tag
|
56
|
+
commit
|
57
|
+
`git tag #{version(v: true)}`
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module Cache
|
3
|
+
CACHE_FILE_NAME = '.xat'
|
4
|
+
|
5
|
+
def save_cache(hash)
|
6
|
+
return if options[:zipfile]
|
7
|
+
|
8
|
+
@cache = File.exist?(cache_path) ? JSON.parse(File.read(@cache_path)).update(hash) : hash
|
9
|
+
File.open(@cache_path, 'w') { |f| f.write JSON.pretty_generate(@cache) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch_cache(key)
|
13
|
+
@cache ||= File.exist?(cache_path) ? JSON.parse(File.read(@cache_path)) : {}
|
14
|
+
@cache[key] if @cache
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_cache
|
18
|
+
File.delete cache_path if options[:clean] && File.exist?(cache_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_path
|
22
|
+
@cache_path ||= File.join options[:path], CACHE_FILE_NAME
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'zip/zip'
|
3
|
+
require 'pathname'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'faraday'
|
7
|
+
require 'io/console'
|
8
|
+
require 'zendesk_apps_support'
|
9
|
+
|
10
|
+
require 'zendesk_apps_tools/command_helpers'
|
11
|
+
|
12
|
+
module ZendeskAppsTools
|
13
|
+
class Command < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
include ZendeskAppsSupport
|
16
|
+
include ZendeskAppsTools::CommandHelpers
|
17
|
+
|
18
|
+
SHARED_OPTIONS = {
|
19
|
+
['path', '-p'] => './',
|
20
|
+
clean: false
|
21
|
+
}
|
22
|
+
|
23
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), '../..'))
|
24
|
+
|
25
|
+
desc 'translate SUBCOMMAND', 'Manage translation files', hide: true
|
26
|
+
subcommand 'translate', Translate
|
27
|
+
|
28
|
+
desc 'bump SUBCOMMAND', 'Bump version for app', hide: true
|
29
|
+
subcommand 'bump', Bump
|
30
|
+
|
31
|
+
desc 'new', 'Generate a new app'
|
32
|
+
method_option :'iframe-only', type: :boolean,
|
33
|
+
default: false,
|
34
|
+
desc: 'Create an iFrame Only app template',
|
35
|
+
aliases: ['-i', '--v2']
|
36
|
+
def new
|
37
|
+
enter = ->(variable) { "Enter this app author's #{variable}:\n" }
|
38
|
+
invalid = ->(variable) { "Invalid #{variable}, try again:" }
|
39
|
+
@author_name = get_value_from_stdin(enter.call('name'),
|
40
|
+
error_msg: invalid.call('name'))
|
41
|
+
@author_email = get_value_from_stdin(enter.call('email'),
|
42
|
+
valid_regex: /^.+@.+\..+$/,
|
43
|
+
error_msg: invalid.call('email'))
|
44
|
+
@author_url = get_value_from_stdin(enter.call('url'),
|
45
|
+
valid_regex: %r{^https?://.+$},
|
46
|
+
error_msg: invalid.call('url'),
|
47
|
+
allow_empty: true)
|
48
|
+
@app_name = get_value_from_stdin("Enter a name for this new app:\n",
|
49
|
+
error_msg: invalid.call('app name'))
|
50
|
+
|
51
|
+
@iframe_location = if options[:'iframe-only']
|
52
|
+
iframe_uri_text = 'Enter your iFrame URI or leave it blank to use'\
|
53
|
+
" a default local template page:\n"
|
54
|
+
value = get_value_from_stdin(iframe_uri_text, allow_empty: true)
|
55
|
+
value == '' ? 'assets/iframe.html' : value
|
56
|
+
else
|
57
|
+
'_legacy'
|
58
|
+
end
|
59
|
+
|
60
|
+
prompt_new_app_dir
|
61
|
+
|
62
|
+
skeleton = options[:'iframe-only'] ? 'app_template_iframe' : 'app_template'
|
63
|
+
is_custom_iframe = options[:'iframe-only'] && @iframe_location != 'assets/iframe.html'
|
64
|
+
directory_options = is_custom_iframe ? { exclude_pattern: /iframe.html/ } : {}
|
65
|
+
directory(skeleton, @app_dir, directory_options)
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'validate', 'Validate your app'
|
69
|
+
method_options SHARED_OPTIONS
|
70
|
+
def validate
|
71
|
+
setup_path(options[:path])
|
72
|
+
errors = app_package.validate(marketplace: false)
|
73
|
+
valid = errors.none?
|
74
|
+
|
75
|
+
if valid
|
76
|
+
app_package.warnings.each { |w| say w.to_s, :yellow }
|
77
|
+
say_status 'validate', 'OK'
|
78
|
+
else
|
79
|
+
errors.each do |e|
|
80
|
+
say_status 'validate', e.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@destination_stack.pop if options[:path]
|
85
|
+
exit 1 unless valid
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'package', 'Package your app'
|
90
|
+
method_options SHARED_OPTIONS
|
91
|
+
def package
|
92
|
+
return false unless invoke(:validate, [])
|
93
|
+
|
94
|
+
setup_path(options[:path])
|
95
|
+
archive_path = File.join(tmp_dir, "app-#{Time.now.strftime('%Y%m%d%H%M%S')}.zip")
|
96
|
+
|
97
|
+
archive_rel_path = relative_to_original_destination_root(archive_path)
|
98
|
+
|
99
|
+
zip archive_path
|
100
|
+
|
101
|
+
say_status 'package', "created at #{archive_rel_path}"
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
desc 'clean', 'Remove app packages in temp folder'
|
106
|
+
method_option :path, default: './', required: false, aliases: '-p'
|
107
|
+
def clean
|
108
|
+
setup_path(options[:path])
|
109
|
+
|
110
|
+
return unless File.exist?(Pathname.new(File.join(app_dir, 'tmp')).to_s)
|
111
|
+
|
112
|
+
FileUtils.rm(Dir["#{tmp_dir}/app-*.zip"])
|
113
|
+
end
|
114
|
+
|
115
|
+
DEFAULT_SERVER_PATH = './'
|
116
|
+
DEFAULT_CONFIG_PATH = './settings.yml'
|
117
|
+
DEFAULT_SERVER_PORT = 4567
|
118
|
+
DEFAULT_APP_ID = 0
|
119
|
+
|
120
|
+
desc 'server', 'Run a http server to serve the local app'
|
121
|
+
method_option :path, default: DEFAULT_SERVER_PATH, required: false, aliases: '-p'
|
122
|
+
method_option :config, default: DEFAULT_CONFIG_PATH, required: false, aliases: '-c'
|
123
|
+
method_option :port, default: DEFAULT_SERVER_PORT, required: false
|
124
|
+
method_option :app_id, default: DEFAULT_APP_ID, required: false
|
125
|
+
def server
|
126
|
+
setup_path(options[:path])
|
127
|
+
manifest = app_package.manifest_json
|
128
|
+
|
129
|
+
settings_helper = ZendeskAppsTools::Settings.new
|
130
|
+
|
131
|
+
settings = settings_helper.get_settings_from_file options[:config], manifest['parameters']
|
132
|
+
|
133
|
+
unless settings
|
134
|
+
settings = settings_helper.get_settings_from_user_input self, manifest['parameters']
|
135
|
+
end
|
136
|
+
|
137
|
+
require 'zendesk_apps_tools/server'
|
138
|
+
ZendeskAppsTools::Server.tap do |server|
|
139
|
+
server.set :port, options[:port]
|
140
|
+
server.set :root, options[:path]
|
141
|
+
server.set :parameters, settings
|
142
|
+
server.set :manifest, manifest['parameters']
|
143
|
+
server.set :config, options[:config]
|
144
|
+
server.set :app_id, options[:app_id]
|
145
|
+
server.run!
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
desc 'create', 'Create app on your account'
|
150
|
+
method_options SHARED_OPTIONS
|
151
|
+
method_option :zipfile, default: nil, required: false, type: :string
|
152
|
+
def create
|
153
|
+
clear_cache
|
154
|
+
@command = 'Create'
|
155
|
+
|
156
|
+
unless options[:zipfile]
|
157
|
+
app_name = JSON.parse(File.read(File.join options[:path], 'manifest.json'))['name']
|
158
|
+
end
|
159
|
+
app_name ||= get_value_from_stdin('Enter app name:')
|
160
|
+
deploy_app(:post, '/api/v2/apps.json', name: app_name)
|
161
|
+
end
|
162
|
+
|
163
|
+
desc 'update', 'Update app on the server'
|
164
|
+
method_options SHARED_OPTIONS
|
165
|
+
method_option :zipfile, default: nil, required: false, type: :string
|
166
|
+
def update
|
167
|
+
clear_cache
|
168
|
+
@command = 'Update'
|
169
|
+
|
170
|
+
app_id = fetch_cache('app_id') || find_app_id
|
171
|
+
unless /\d+/ =~ app_id.to_s
|
172
|
+
say_error_and_exit "App id not found\nPlease try running command with --clean or check your internet connection"
|
173
|
+
end
|
174
|
+
deploy_app(:put, "/api/v2/apps/#{app_id}.json", {})
|
175
|
+
end
|
176
|
+
|
177
|
+
protected
|
178
|
+
|
179
|
+
def setup_path(path)
|
180
|
+
@destination_stack << relative_to_original_destination_root(path) unless @destination_stack.last == path
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|