bookbindery 1.0.3 → 2.0.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 +4 -4
- data/{bin → install_bin}/bookbinder +0 -0
- data/lib/bookbinder.rb +2 -9
- data/lib/bookbinder/archive.rb +1 -1
- data/lib/bookbinder/archive_menu_configuration.rb +34 -0
- data/lib/bookbinder/book.rb +17 -17
- data/lib/bookbinder/cli.rb +25 -36
- data/lib/bookbinder/code_example.rb +5 -38
- data/lib/bookbinder/code_example_reader.rb +40 -0
- data/lib/bookbinder/colorizer.rb +14 -0
- data/lib/bookbinder/command_runner.rb +9 -16
- data/lib/bookbinder/command_validator.rb +14 -7
- data/lib/bookbinder/commands/bind.rb +321 -0
- data/lib/bookbinder/commands/build_and_push_tarball.rb +7 -6
- data/lib/bookbinder/commands/chain.rb +11 -0
- data/lib/bookbinder/commands/generate_pdf.rb +4 -3
- data/lib/bookbinder/commands/help.rb +49 -10
- data/lib/bookbinder/commands/naming.rb +9 -1
- data/lib/bookbinder/commands/push_local_to_staging.rb +4 -3
- data/lib/bookbinder/commands/push_to_prod.rb +36 -4
- data/lib/bookbinder/commands/run_publish_ci.rb +21 -24
- data/lib/bookbinder/commands/tag.rb +3 -3
- data/lib/bookbinder/commands/update_local_doc_repos.rb +5 -4
- data/lib/bookbinder/commands/version.rb +11 -8
- data/lib/bookbinder/configuration.rb +8 -3
- data/lib/bookbinder/configuration_fetcher.rb +7 -25
- data/lib/bookbinder/configuration_validator.rb +21 -0
- data/lib/bookbinder/distributor.rb +1 -1
- data/lib/bookbinder/dita_html_to_middleman_formatter.rb +37 -0
- data/lib/bookbinder/dita_section.rb +7 -0
- data/lib/bookbinder/dita_section_gatherer.rb +28 -0
- data/lib/bookbinder/git_accessor.rb +17 -0
- data/lib/bookbinder/git_client.rb +10 -7
- data/lib/bookbinder/git_hub_repository.rb +46 -41
- data/lib/bookbinder/local_dita_preprocessor.rb +27 -0
- data/lib/bookbinder/local_dita_to_html_converter.rb +49 -0
- data/lib/bookbinder/local_file_system_accessor.rb +68 -0
- data/lib/bookbinder/middleman_runner.rb +30 -17
- data/lib/bookbinder/publisher.rb +16 -80
- data/lib/bookbinder/remote_yaml_credential_provider.rb +2 -3
- data/lib/bookbinder/repositories/command_repository.rb +156 -0
- data/lib/bookbinder/repositories/section_repository.rb +31 -0
- data/lib/bookbinder/section.rb +5 -67
- data/lib/bookbinder/shell_out.rb +1 -0
- data/lib/bookbinder/sheller.rb +19 -0
- data/lib/bookbinder/sieve.rb +6 -1
- data/lib/bookbinder/terminal.rb +10 -0
- data/lib/bookbinder/user_message.rb +6 -0
- data/lib/bookbinder/user_message_presenter.rb +21 -0
- data/lib/bookbinder/yaml_loader.rb +18 -7
- data/master_middleman/archive_drop_down_menu.rb +46 -0
- data/master_middleman/bookbinder_helpers.rb +47 -40
- metadata +33 -87
- data/lib/bookbinder/commands/publish.rb +0 -138
- data/lib/bookbinder/usage_messenger.rb +0 -33
@@ -127,11 +127,31 @@ module Bookbinder
|
|
127
127
|
end
|
128
128
|
end
|
129
129
|
|
130
|
+
class RequiredKeysChecker
|
131
|
+
def check(config)
|
132
|
+
missing_keys = []
|
133
|
+
|
134
|
+
Configuration::CONFIG_REQUIRED_KEYS.map do |required_key|
|
135
|
+
config_keys = config.keys
|
136
|
+
unless config_keys.include?(required_key)
|
137
|
+
missing_keys.push(required_key)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
if missing_keys.length > 0
|
142
|
+
raise ConfigurationValidator::MissingRequiredKeyError.new(
|
143
|
+
"Your config.yml is missing required key(s). Required keys are #{missing_keys.join(", ")}."
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
130
149
|
class ConfigurationValidator
|
131
150
|
DuplicateSectionNameError = Class.new(RuntimeError)
|
132
151
|
MissingArchiveMenuPartialError = Class.new(RuntimeError)
|
133
152
|
EmptyArchiveItemsError = Class.new(RuntimeError)
|
134
153
|
ArchiveMenuNotDefinedError = Class.new(RuntimeError)
|
154
|
+
MissingRequiredKeyError = Class.new(RuntimeError)
|
135
155
|
|
136
156
|
def initialize(logger, file_system_accessor)
|
137
157
|
@logger = logger
|
@@ -143,6 +163,7 @@ module Bookbinder
|
|
143
163
|
|
144
164
|
user_config_schema_version = config_hash['schema_version']
|
145
165
|
exceptions = [
|
166
|
+
RequiredKeysChecker.new,
|
146
167
|
ConfigVersionChecker.new(Version.parse(bookbinder_schema_version),
|
147
168
|
Version.parse(starting_schema_version),
|
148
169
|
VersionCheckerMessages.new(Version.parse(user_config_schema_version),
|
@@ -5,7 +5,7 @@ module Bookbinder
|
|
5
5
|
EXPIRATION_HOURS = 2
|
6
6
|
|
7
7
|
def self.build(logger, options)
|
8
|
-
namespace = GitHubRepository.new(logger: logger, full_name: options[:book_repo]).short_name
|
8
|
+
namespace = GitHubRepository.new(logger: logger, full_name: options[:book_repo], git_accessor: Git).short_name
|
9
9
|
namer = ArtifactNamer.new(namespace, options[:build_number], 'log', '/tmp')
|
10
10
|
|
11
11
|
archive = Archive.new(logger: logger, key: options[:aws_credentials].access_key, secret: options[:aws_credentials].secret_key)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
|
3
|
+
class DitaHtmlToMiddlemanFormatter
|
4
|
+
def initialize(file_system_accessor)
|
5
|
+
@file_system_accessor = file_system_accessor
|
6
|
+
end
|
7
|
+
|
8
|
+
def format(src, dest)
|
9
|
+
all_files_with_ext = file_system_accessor.find_files_with_ext('.html', src)
|
10
|
+
|
11
|
+
all_files_with_ext.map do |filepath|
|
12
|
+
file_title_text = file_system_accessor.read_html_in_tag(path: filepath,
|
13
|
+
marker: 'title')
|
14
|
+
|
15
|
+
file_body_text = file_system_accessor.read_html_in_tag(path: filepath,
|
16
|
+
marker: 'body')
|
17
|
+
|
18
|
+
relative_path_to_file = file_system_accessor.relative_path_from(src, filepath)
|
19
|
+
new_filepath = File.join dest, "#{relative_path_to_file}.erb"
|
20
|
+
|
21
|
+
output_text = frontmatter(file_title_text) + file_body_text
|
22
|
+
|
23
|
+
file_system_accessor.write(to: new_filepath, text: output_text)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :file_system_accessor
|
30
|
+
|
31
|
+
def frontmatter(title)
|
32
|
+
sanitized_title = title.gsub('"', '\"')
|
33
|
+
"---\ntitle: \"#{sanitized_title}\"\ndita: true\n---\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
class DitaSectionGatherer
|
3
|
+
def initialize(version_control_system, view_updater)
|
4
|
+
@version_control_system = version_control_system
|
5
|
+
@view_updater = view_updater
|
6
|
+
end
|
7
|
+
|
8
|
+
def gather(dita_sections, to: nil)
|
9
|
+
dita_sections.map do |dita_section|
|
10
|
+
view_updater.log "Gathering " + "#{dita_section.full_name}".cyan
|
11
|
+
version_control_system.clone("git@github.com:#{dita_section.full_name}",
|
12
|
+
dita_section.directory,
|
13
|
+
path: to)
|
14
|
+
|
15
|
+
DitaSection.new(File.join(to, dita_section.directory),
|
16
|
+
dita_section.ditamap_location,
|
17
|
+
dita_section.full_name,
|
18
|
+
dita_section.target_ref,
|
19
|
+
dita_section.directory)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :version_control_system, :view_updater
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class GitAccessor
|
5
|
+
def initialize
|
6
|
+
@cache = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def clone(url, name, path: nil)
|
10
|
+
cache[[url, name, path]] ||= Git.clone(url, name, path: path)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :cache, :third_party
|
16
|
+
end
|
17
|
+
end
|
@@ -5,18 +5,21 @@ module Bookbinder
|
|
5
5
|
class GitClient::TokenException < StandardError;
|
6
6
|
end
|
7
7
|
|
8
|
-
def initialize(logger, *args)
|
9
|
-
@logger = logger
|
10
|
-
super(*args)
|
11
|
-
end
|
12
|
-
|
13
8
|
def head_sha(full_name)
|
14
9
|
commits(full_name).first.sha
|
15
10
|
end
|
16
11
|
|
17
12
|
def create_tag!(full_name, tagname, ref)
|
18
|
-
|
19
|
-
|
13
|
+
tag_result = create_tag(
|
14
|
+
full_name,
|
15
|
+
"tags/#{tagname}",
|
16
|
+
'Tagged by Bookbinder',
|
17
|
+
ref,
|
18
|
+
'commit',
|
19
|
+
'Bookbinder',
|
20
|
+
'bookbinder@cloudfoundry.org',
|
21
|
+
Time.now.iso8601
|
22
|
+
)
|
20
23
|
create_ref(full_name, "tags/#{tagname}", tag_result.sha)
|
21
24
|
rescue Octokit::Unauthorized, Octokit::NotFound
|
22
25
|
raise_error_with_context
|
@@ -1,52 +1,65 @@
|
|
1
|
-
require 'ruby-progressbar'
|
2
1
|
require 'bookbinder/shell_out'
|
3
2
|
require 'git'
|
4
|
-
require_relative 'git_client'
|
5
3
|
require_relative 'bookbinder_logger'
|
4
|
+
require_relative 'git_client'
|
6
5
|
|
7
6
|
module Bookbinder
|
8
7
|
class GitHubRepository
|
9
|
-
|
10
|
-
def initialize(msg=nil)
|
11
|
-
super
|
12
|
-
end
|
13
|
-
end
|
8
|
+
RepositoryCloneError = Class.new(StandardError)
|
14
9
|
|
15
10
|
include Bookbinder::ShellOut
|
16
11
|
|
17
12
|
attr_reader :full_name, :copied_to
|
18
13
|
|
19
|
-
def self.build_from_remote(logger,
|
14
|
+
def self.build_from_remote(logger,
|
15
|
+
section_hash,
|
16
|
+
target_ref,
|
17
|
+
git_accessor)
|
20
18
|
full_name = section_hash.fetch('repository', {}).fetch('name')
|
21
19
|
target_ref = target_ref || section_hash.fetch('repository', {})['ref']
|
22
20
|
directory = section_hash['directory']
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
new(logger: logger,
|
22
|
+
full_name: full_name,
|
23
|
+
target_ref: target_ref,
|
24
|
+
github_token: ENV['GITHUB_API_TOKEN'],
|
25
|
+
directory: directory,
|
26
|
+
git_accessor: git_accessor)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_from_local(logger,
|
30
|
+
section_hash,
|
31
|
+
local_repo_dir,
|
32
|
+
git_accessor)
|
29
33
|
full_name = section_hash.fetch('repository').fetch('name')
|
30
34
|
directory = section_hash['directory']
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
+
new(logger: logger,
|
37
|
+
full_name: full_name,
|
38
|
+
directory: directory,
|
39
|
+
local_repo_dir: local_repo_dir,
|
40
|
+
git_accessor: git_accessor)
|
36
41
|
end
|
37
42
|
|
38
|
-
def initialize(logger: nil,
|
43
|
+
def initialize(logger: nil,
|
44
|
+
full_name: nil,
|
45
|
+
target_ref: nil,
|
46
|
+
github_token: nil,
|
47
|
+
directory: nil,
|
48
|
+
local_repo_dir: nil,
|
49
|
+
git_accessor: nil)
|
39
50
|
@logger = logger
|
40
|
-
#TODO better error message
|
41
51
|
raise 'No full_name provided ' unless full_name
|
42
52
|
@full_name = full_name
|
43
|
-
@github = GitClient.new(logger, access_token: github_token || ENV['GITHUB_API_TOKEN'])
|
44
53
|
@target_ref = target_ref
|
45
54
|
@directory = directory
|
46
55
|
@local_repo_dir = local_repo_dir
|
56
|
+
|
57
|
+
@github = GitClient.new(access_token: github_token || ENV['GITHUB_API_TOKEN'])
|
58
|
+
@git_accessor = git_accessor or raise ArgumentError.new("Must provide a git accessor")
|
47
59
|
end
|
48
60
|
|
49
61
|
def tag_with(tagname)
|
62
|
+
@logger.log 'Tagging ' + full_name.cyan
|
50
63
|
@github.create_tag! full_name, tagname, head_sha
|
51
64
|
end
|
52
65
|
|
@@ -62,9 +75,11 @@ module Bookbinder
|
|
62
75
|
@directory || short_name
|
63
76
|
end
|
64
77
|
|
65
|
-
def copy_from_remote(destination_dir
|
78
|
+
def copy_from_remote(destination_dir)
|
66
79
|
begin
|
67
|
-
@
|
80
|
+
@git_base_object = git_accessor.clone("git@github.com:#{full_name}",
|
81
|
+
directory,
|
82
|
+
path: destination_dir)
|
68
83
|
rescue => e
|
69
84
|
if e.message.include? "Permission denied (publickey)"
|
70
85
|
raise RepositoryCloneError.new "Unable to access repository #{full_name}. You do not have the correct access rights. Please either add the key to your SSH agent, or set the GIT_SSH environment variable to override default SSH key usage. For more information run: `man git`."
|
@@ -75,8 +90,8 @@ module Bookbinder
|
|
75
90
|
raise e
|
76
91
|
end
|
77
92
|
end
|
78
|
-
@
|
79
|
-
@copied_to = destination_dir
|
93
|
+
@git_base_object.checkout(target_ref) unless target_ref == 'master'
|
94
|
+
@copied_to = File.join(destination_dir, directory)
|
80
95
|
end
|
81
96
|
|
82
97
|
def copy_from_local(destination_dir)
|
@@ -116,30 +131,20 @@ module Bookbinder
|
|
116
131
|
@logger.log ' skipping (not found) '.magenta + path_to_local_repo
|
117
132
|
end
|
118
133
|
|
119
|
-
def get_modification_date_for(file: nil, git: nil)
|
120
|
-
@git ||= git
|
121
|
-
raise "Unexpected Error: Git accessor unavailable." if @git.nil?
|
122
|
-
|
123
|
-
irrelevant_path_component = directory+'/'
|
124
|
-
repo_path = file.gsub(irrelevant_path_component, '')
|
125
|
-
|
126
|
-
begin
|
127
|
-
@git.log(1).object(repo_path).first.date
|
128
|
-
rescue Git::GitExecuteError => e
|
129
|
-
raise "This file does not exist or is not tracked by git! Cannot get last modified date for #{repo_path}."
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
134
|
def path_to_local_repo
|
134
|
-
|
135
|
+
if @local_repo_dir
|
136
|
+
File.join(@local_repo_dir, short_name)
|
137
|
+
end
|
135
138
|
end
|
136
139
|
|
137
140
|
def has_git_object?
|
138
|
-
!!@
|
141
|
+
!!@git_base_object
|
139
142
|
end
|
140
143
|
|
141
144
|
private
|
142
145
|
|
146
|
+
attr_reader :git_accessor
|
147
|
+
|
143
148
|
def target_ref
|
144
149
|
@target_ref ||= 'master'
|
145
150
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Bookbinder
|
2
|
+
class LocalDitaPreprocessor
|
3
|
+
|
4
|
+
def initialize(dita_converter, dita_formatter, local_file_system_accessor)
|
5
|
+
@dita_converter = dita_converter
|
6
|
+
@dita_formatter = dita_formatter
|
7
|
+
@local_file_system_accessor = local_file_system_accessor
|
8
|
+
end
|
9
|
+
|
10
|
+
def preprocess(dita_sections, converted_dita_dir, formatted_dita_dir, workspace_dir)
|
11
|
+
dita_converter.convert dita_sections, to: converted_dita_dir
|
12
|
+
|
13
|
+
dita_formatter.format converted_dita_dir, formatted_dita_dir
|
14
|
+
|
15
|
+
local_file_system_accessor.copy_named_directory_with_path('images',
|
16
|
+
converted_dita_dir,
|
17
|
+
workspace_dir)
|
18
|
+
local_file_system_accessor.copy_contents(formatted_dita_dir, workspace_dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :dita_converter, :dita_formatter, :local_file_system_accessor
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative '../bookbinder/dita_section'
|
2
|
+
|
3
|
+
module Bookbinder
|
4
|
+
class LocalDitaToHtmlConverter
|
5
|
+
DitaToHtmlLibraryFailure = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
def initialize(sheller, path_to_dita_ot_library)
|
8
|
+
@sheller = sheller
|
9
|
+
@path_to_dita_ot_library = path_to_dita_ot_library
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert(dita_sections, to: nil)
|
13
|
+
dita_sections.map do |dita_section|
|
14
|
+
absolute_path_to_ditamap = File.join dita_section.path_to_local_repo, dita_section.ditamap_location
|
15
|
+
classpath = "#{path_to_dita_ot_library}/lib/xercesImpl.jar:" +
|
16
|
+
"#{path_to_dita_ot_library}/lib/xml-apis.jar:" +
|
17
|
+
"#{path_to_dita_ot_library}/lib/resolver.jar:" +
|
18
|
+
"#{path_to_dita_ot_library}/lib/commons-codec-1.4.jar:" +
|
19
|
+
"#{path_to_dita_ot_library}/lib/icu4j.jar:" +
|
20
|
+
"#{path_to_dita_ot_library}/lib/saxon/saxon9-dom.jar:" +
|
21
|
+
"#{path_to_dita_ot_library}/lib/saxon/saxon9.jar:target/classes:" +
|
22
|
+
"#{path_to_dita_ot_library}:" +
|
23
|
+
"#{path_to_dita_ot_library}/lib/:" +
|
24
|
+
"#{path_to_dita_ot_library}/lib/dost.jar"
|
25
|
+
out_dir = File.join to, dita_section.directory
|
26
|
+
command = "export CLASSPATH=#{classpath}; " +
|
27
|
+
"ant -f #{path_to_dita_ot_library} " +
|
28
|
+
"-Dbasedir='/' " +
|
29
|
+
"-Doutput.dir=#{out_dir} " +
|
30
|
+
"-Dtranstype='tocjs' " +
|
31
|
+
"-Dargs.input=#{absolute_path_to_ditamap} "
|
32
|
+
|
33
|
+
begin
|
34
|
+
sheller.run_command(command)
|
35
|
+
rescue Sheller::ShelloutFailure
|
36
|
+
raise DitaToHtmlLibraryFailure.new 'The DITA-to-HTML conversion failed. ' +
|
37
|
+
'Please check that you have specified the path to your DITA-OT library in the ENV, ' +
|
38
|
+
'that your DITA-specific keys/values in config.yml are set, ' +
|
39
|
+
'and that your DITA toolkit is correctly configured.'
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :sheller, :path_to_dita_ot_library
|
48
|
+
end
|
49
|
+
end
|
@@ -1,9 +1,77 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'pathname'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
1
5
|
module Bookbinder
|
2
6
|
|
3
7
|
class LocalFileSystemAccessor
|
4
8
|
def file_exist?(path)
|
5
9
|
File.exist?(path)
|
6
10
|
end
|
11
|
+
|
12
|
+
def write(to: nil, text: nil)
|
13
|
+
make_directory(File.dirname to)
|
14
|
+
|
15
|
+
File.open(to, 'a') do |f|
|
16
|
+
f.write(text)
|
17
|
+
end
|
18
|
+
|
19
|
+
to
|
20
|
+
end
|
21
|
+
|
22
|
+
def read(path)
|
23
|
+
File.read(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_html_in_tag(path: nil, marker: nil)
|
27
|
+
doc = Nokogiri::XML(File.open path)
|
28
|
+
doc.css(marker).inner_html
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_directory(path)
|
32
|
+
FileUtils.rm_rf(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def make_directory(path)
|
36
|
+
FileUtils.mkdir_p(path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def copy(src, dest)
|
40
|
+
FileUtils.cp_r src, dest
|
41
|
+
end
|
42
|
+
|
43
|
+
def copy_contents(src, dest)
|
44
|
+
contents = Dir.glob File.join(src, '**')
|
45
|
+
contents.each do |dir|
|
46
|
+
FileUtils.cp_r dir, dest
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def copy_named_directory_with_path(dir_name, src, dest)
|
51
|
+
contents = Dir.glob File.join(src, "**/#{dir_name}")
|
52
|
+
contents.each do |dir|
|
53
|
+
relative_path_to_dir = relative_path_from(src, dir)
|
54
|
+
extended_dest = File.join dest, relative_path_to_dir
|
55
|
+
FileUtils.mkdir_p extended_dest
|
56
|
+
copy_contents dir, extended_dest
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def rename_file(path, new_name)
|
61
|
+
new_path = File.expand_path File.join path, '..', new_name
|
62
|
+
File.rename(path, new_path)
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_files_with_ext(ext, path)
|
66
|
+
Dir[File.join path, '**/*'].select { |file| File.basename(file).match(ext) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def relative_path_from(src, target)
|
70
|
+
target_path = Pathname(File.absolute_path target)
|
71
|
+
relative_path = target_path.relative_path_from(Pathname(File.absolute_path src))
|
72
|
+
relative_path.to_s
|
73
|
+
end
|
74
|
+
|
7
75
|
end
|
8
76
|
|
9
77
|
end
|