bookbindery 1.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|