xcunique 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/Guardfile +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +73 -0
- data/Rakefile +11 -0
- data/bin/xcunique +5 -0
- data/lib/json2plist/json2plist +21 -0
- data/lib/xcunique.rb +23 -0
- data/lib/xcunique/cli.rb +40 -0
- data/lib/xcunique/hash_ext.rb +25 -0
- data/lib/xcunique/helpers.rb +26 -0
- data/lib/xcunique/options.rb +68 -0
- data/lib/xcunique/parser.rb +54 -0
- data/lib/xcunique/sorter.rb +51 -0
- data/lib/xcunique/uniquifier.rb +36 -0
- data/lib/xcunique/version.rb +3 -0
- data/spec/fixtures/TestProject.json +548 -0
- data/spec/fixtures/TestProject/TestProject.xcodeproj/project.pbxproj +510 -0
- data/spec/fixtures/TestProject/TestProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- data/spec/fixtures/TestProject/TestProject.xcodeproj/project.xcworkspace/xcuserdata/paul.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- data/spec/fixtures/TestProject/TestProject.xcodeproj/xcuserdata/paul.xcuserdatad/xcschemes/TestProject.xcscheme +111 -0
- data/spec/fixtures/TestProject/TestProject.xcodeproj/xcuserdata/paul.xcuserdatad/xcschemes/xcschememanagement.plist +47 -0
- data/spec/fixtures/TestProject/TestProject/AppDelegate.swift +46 -0
- data/spec/fixtures/TestProject/TestProject/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- data/spec/fixtures/TestProject/TestProject/Base.lproj/LaunchScreen.storyboard +27 -0
- data/spec/fixtures/TestProject/TestProject/Base.lproj/Main.storyboard +26 -0
- data/spec/fixtures/TestProject/TestProject/Info.plist +47 -0
- data/spec/fixtures/TestProject/TestProject/ViewController.swift +25 -0
- data/spec/fixtures/TestProject/TestProjectTests/Info.plist +24 -0
- data/spec/fixtures/TestProject/TestProjectTests/TestProjectTests.swift +36 -0
- data/spec/fixtures/TestProject/TestProjectUITests/Info.plist +24 -0
- data/spec/fixtures/TestProject/TestProjectUITests/TestProjectUITests.swift +36 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/xcunique/hash_ext_spec.rb +36 -0
- data/spec/xcunique/helpers_spec.rb +58 -0
- data/spec/xcunique/parser_spec.rb +214 -0
- data/spec/xcunique/sorter_spec.rb +116 -0
- data/xcunique.gemspec +26 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 83fc12d810e92304f2b2a4dffd93e6774aa2857a
|
4
|
+
data.tar.gz: 6083cfdbeb9ecb2c2f2407b410212390299de884
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c2e39e840ef36829b57e021097a01fce015ab8d407b01670783286420823e2f9b77a13f882f2575447b2c8e5a0d5e5f437c1ca5326bd2e2db2deac77df4e5257
|
7
|
+
data.tar.gz: 60a34ec352da66c7e7ca1c6f01a5c529c4348a0e828a359f12c04e356f02b94497be3f19797e4d018b8f6fea81c52f77fa210697026a46cc6e315d6f75cccce4
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
.DS_Store
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
guard :minitest do
|
19
|
+
watch(%r{^spec/(.*)\/?(.*)_spec\.rb$})
|
20
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
21
|
+
watch(%r{^spec/spec_helper\.rb$}) { 'test' }
|
22
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Paul Samuels
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# `xcunique`
|
2
|
+
|
3
|
+
A tool to reduce merge conflicts with Xcode projects by creating deterministic UUIDs for every element within the `project.pbxproj` file.
|
4
|
+
|
5
|
+
This is initially a fun yak shaving task as existing tools, such as [xUnique](https://github.com/truebit/xUnique), already exist. There is a vague idea that this would be a part of a very small suite of tools to help reduce the pain of managing Xcode projects.
|
6
|
+
|
7
|
+
###Warning
|
8
|
+
|
9
|
+
If you are not using source control then you should not be using this tool.
|
10
|
+
|
11
|
+
---
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'xcunique'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install xcunique
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
```
|
30
|
+
Usage: xcunique PROJECT.PBXPROJ [options]
|
31
|
+
-f, --format=<ascii|json|xml> ascii|json|xml
|
32
|
+
-v, --verbose
|
33
|
+
```
|
34
|
+
|
35
|
+
###`--format`
|
36
|
+
The format switch has 3 options
|
37
|
+
|
38
|
+
* `ascii` requires [`xcproj`](https://github.com/0xced/xcproj) to be installed. It writes the project to a plist and then touches it with [`xcproj`](https://github.com/0xced/xcproj)
|
39
|
+
* `json` outputs to stdout - useful for debugging or using with other tools
|
40
|
+
* `xml` writes over the project file with an XML formatted plist
|
41
|
+
|
42
|
+
###`--verbose`
|
43
|
+
|
44
|
+
* It's always nice to now what is happening so show some logs
|
45
|
+
|
46
|
+
---
|
47
|
+
|
48
|
+
## What does it even do?
|
49
|
+
|
50
|
+
Good question - there is not a lot of source code to look at but I'll call out the general process.
|
51
|
+
|
52
|
+
[cli.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/cli.rb) is where all the fun begins:
|
53
|
+
https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/cli.rb
|
54
|
+
* It uses [options.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/options.rb) to extract the command line arguments and do some basic checking to ensure you have provided valid paths etc.
|
55
|
+
* Then it uses [uniquifier.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/uniquifier.rb) to generate the deterministic UUIDs
|
56
|
+
* Then it uses [sorter.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/sorter.rb) to sort the groups/files alphabetically
|
57
|
+
* The uniqued/sorted files is then written out to a JSON file
|
58
|
+
* This JSON file is converted to an XML plist using the bundled swift script
|
59
|
+
* This plist is then optionally converted to ascii using [`xcproj`](https://github.com/0xced/xcproj)
|
60
|
+
|
61
|
+
[uniquifier.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/uniquifier.rb) coordinates the majority of the process:
|
62
|
+
|
63
|
+
* It creates a [parser.rb](https://github.com/paulsamuels/xcunique/blob/master/lib/xcunique/parser.rb) that first enumerates the groups/files section of the Xcode project and then traverses from the root object in the project. The parser keeps track of UUIDs that it has already visited, which prevents getting into cycles and generating different paths if files are included in multiple targets.
|
64
|
+
* The result of this parsing is a hash of substitutions going from old UUID to new deterministic UUID - the transforming from old UUID to new UUID is performed during a deep clone of the project object
|
65
|
+
|
66
|
+
To see the format of the normalised paths before they are MD5'd check out the tests in [parser_spec.rb](https://github.com/paulsamuels/xcunique/blob/master/spec/xcunique/sorter.rb)
|
67
|
+
## Contributing
|
68
|
+
|
69
|
+
1. Fork it ( https://github.com/[my-github-username]/xcunique/fork )
|
70
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
71
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
72
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
73
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/xcunique
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env xcrun swift
|
2
|
+
|
3
|
+
import Foundation
|
4
|
+
|
5
|
+
guard Process.arguments.count == 3 else {
|
6
|
+
print("Usage: json2plist path/to/input path/to/output")
|
7
|
+
exit(EXIT_FAILURE)
|
8
|
+
}
|
9
|
+
|
10
|
+
guard let inputData = NSData(contentsOfURL: NSURL(fileURLWithPath: Process.arguments[1])) else {
|
11
|
+
print("Could not load \"\(Process.arguments[1])\"")
|
12
|
+
exit(EXIT_FAILURE)
|
13
|
+
}
|
14
|
+
|
15
|
+
do {
|
16
|
+
if let data = try NSJSONSerialization.JSONObjectWithData(inputData, options: .AllowFragments) as? NSDictionary {
|
17
|
+
data.writeToURL(NSURL(fileURLWithPath: Process.arguments[2]), atomically: true)
|
18
|
+
}
|
19
|
+
} catch {
|
20
|
+
print(error)
|
21
|
+
}
|
data/lib/xcunique.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Xcunique
|
2
|
+
module Keys
|
3
|
+
CHILDREN = 'children'
|
4
|
+
FILE_REF = 'fileRef'
|
5
|
+
FILES = 'files'
|
6
|
+
ISA = 'isa'
|
7
|
+
MAIN_GROUP = 'mainGroup'
|
8
|
+
NAME = 'name'
|
9
|
+
OBJECTS = 'objects'
|
10
|
+
PATH = 'path'
|
11
|
+
PBXGroup = 'PBXGroup'
|
12
|
+
ROOT_OBJECT = 'rootObject'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require "xcunique/cli"
|
17
|
+
require 'xcunique/hash_ext'
|
18
|
+
require 'xcunique/helpers'
|
19
|
+
require "xcunique/options"
|
20
|
+
require "xcunique/parser"
|
21
|
+
require "xcunique/sorter"
|
22
|
+
require "xcunique/uniquifier"
|
23
|
+
require "xcunique/version"
|
data/lib/xcunique/cli.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module Xcunique
|
4
|
+
module CLI
|
5
|
+
def self.run argv
|
6
|
+
options = Options.parse(argv)
|
7
|
+
|
8
|
+
puts %Q{Parsing "#{options.project_path}"} if options.verbose
|
9
|
+
result = Sorter.new(Uniquifier.new(options.project_path).uniquify).sort
|
10
|
+
|
11
|
+
case options.format
|
12
|
+
when :json
|
13
|
+
puts JSON.pretty_generate(result)
|
14
|
+
when :xml, :ascii
|
15
|
+
Dir.mktmpdir do |dir|
|
16
|
+
json_path = dir + '/project.json'
|
17
|
+
puts %Q{Writing uniqued JSON to "#{json_path}"} if options.verbose
|
18
|
+
File.open(json_path, 'w') do |file|
|
19
|
+
file.puts result.to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
json2plist = File.expand_path('../json2plist/json2plist', __dir__)
|
23
|
+
|
24
|
+
puts %Q{Running "#{json2plist} #{json_path} #{options.project_path}"} if options.verbose
|
25
|
+
system File.expand_path('../json2plist/json2plist', __dir__), json_path, options.project_path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
if options.format == :ascii
|
30
|
+
Dir.chdir File.dirname(options.project_path) do
|
31
|
+
puts %Q{Running "xcproj touch" in "#{File.dirname(options.project_path)}"} if options.verbose
|
32
|
+
system 'xcproj', 'touch'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
rescue Options::NoProjectProvidedError, Options::UnknownFormatError, Options::MissingDependencyError => error
|
37
|
+
puts error.message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Performs a deep clone of the object with optional substitutions
|
4
|
+
#
|
5
|
+
# @param substitutions [Hash] optional mapping of substitutions.
|
6
|
+
# If provided during copying any matching substitutions.key in the original will be replaced with the substitutions.value in the clone
|
7
|
+
# @param object [Object] the object to duplicate. This is a recursive call and will continue until resolving to a [String] or [Numeric]
|
8
|
+
# @return [Hash] the deeply copyed object with any substitutions performed
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# { 'needle' => 'test' }.deep_dup(substitutions: { 'needle' => 'dog' }) #=> { 'dog' => 'test' }
|
12
|
+
def deep_dup substitutions: {}, object: self
|
13
|
+
recurse = -> object { deep_dup substitutions: substitutions, object: object }
|
14
|
+
|
15
|
+
case object
|
16
|
+
when Hash
|
17
|
+
Hash[object.map(&recurse)]
|
18
|
+
when Array
|
19
|
+
object.map(&recurse)
|
20
|
+
when String, Numeric
|
21
|
+
substitutions[object] || object
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Xcunique
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Returns the `name` and `path` components of a node
|
5
|
+
#
|
6
|
+
# If the object contains a `fileRef` key then this is traversed to get
|
7
|
+
# the `name`/`path` attributes from the reference file.
|
8
|
+
#
|
9
|
+
# @param uuid [String] the UUID of the object to resolve the attributes for
|
10
|
+
# @param objects [Hash] the objects collection in the project
|
11
|
+
# @return [String] the resolved attributes or an empty string
|
12
|
+
#
|
13
|
+
# @example Valid names are of the form
|
14
|
+
# ''
|
15
|
+
# (name: 'Some Name')
|
16
|
+
# (path: 'Some Path')
|
17
|
+
# (name: 'Some Name', path: 'Some Path')
|
18
|
+
def self.resolve_attributes uuid, objects
|
19
|
+
object = objects[objects[uuid][Keys::FILE_REF] || uuid]
|
20
|
+
|
21
|
+
components = object.sort.select { |key, _| [ Keys::NAME, Keys::PATH ].include?(key) }.map { |key, value| "#{key}: '#{value}'" }.join(", ")
|
22
|
+
components.length > 0 ? %Q{(#{components})} : ''
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Xcunique
|
4
|
+
|
5
|
+
# Options wraps the setup/running of an `OptionParser` and store the results
|
6
|
+
class Options
|
7
|
+
|
8
|
+
# Exception representing the lack of a dependency e.g. `xcproj`
|
9
|
+
class MissingDependencyError < StandardError; end
|
10
|
+
|
11
|
+
# Exception representing missing/invalid first argument which should be the project file
|
12
|
+
class NoProjectProvidedError < StandardError; end
|
13
|
+
|
14
|
+
# Exception representing an incorrect format has been selected
|
15
|
+
class UnknownFormatError < StandardError; end
|
16
|
+
|
17
|
+
# @!attribute format
|
18
|
+
# @return [Symbol] The requested format must be ascii, json or xml - defaults to :ascii
|
19
|
+
# @!attribute project_path
|
20
|
+
# @return [String] The path to the project.pbxproj - this will undergo file expansion
|
21
|
+
# @!attribute verbose
|
22
|
+
# @return [Boolean] representing whether logging should be enabled - defaults to false
|
23
|
+
attr_accessor :format, :project_path, :verbose
|
24
|
+
|
25
|
+
# Parse array of options provided
|
26
|
+
#
|
27
|
+
# @return [Options] an Options object that is configured with the provided command line arguments
|
28
|
+
def self.parse argv
|
29
|
+
new.tap do |options|
|
30
|
+
option_parser = options.send :option_parser
|
31
|
+
|
32
|
+
option_parser.parse!(argv)
|
33
|
+
|
34
|
+
raise NoProjectProvidedError.new(option_parser.banner) unless path = argv.first
|
35
|
+
|
36
|
+
options.project_path = File.expand_path(path)
|
37
|
+
|
38
|
+
raise NoProjectProvidedError.new(option_parser.banner) unless File.exist?(options.project_path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
self.format = :ascii
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def option_parser
|
49
|
+
@option_parser ||= begin
|
50
|
+
OptionParser.new do |opts|
|
51
|
+
opts.banner = "Usage: xcunique PROJECT.PBXPROJ [options]"
|
52
|
+
|
53
|
+
opts.on('-f', '--format=<ascii|json|xml>', 'ascii|json|xml') do |format|
|
54
|
+
self.format = format.chomp.to_sym
|
55
|
+
raise UnknownFormatError.new(%Q{Unknown format "#{format}" - please choose ascii, json or xml}) unless %i{ascii json xml}.include?(self.format)
|
56
|
+
raise MissingDependencyError.new("xcproj is required for converting to ascii") if self.format == :ascii && begin system("command -v xcproj 2&> /dev/null"); !$?.success? end
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on('-v', '--verbose') do |verbose|
|
60
|
+
self.verbose = verbose
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Xcunique
|
2
|
+
|
3
|
+
# Parser is responsible for traversing the project objects from the entry point provided
|
4
|
+
# and building a Hash of visited notes which map old UUID to path of node.
|
5
|
+
#
|
6
|
+
# Nodes that have already been visited are skipped
|
7
|
+
class Parser
|
8
|
+
|
9
|
+
# @!attribute [r] visited
|
10
|
+
# @return [Hash] a mapping of old UUID to the of where the node is situated
|
11
|
+
attr_reader :visited
|
12
|
+
|
13
|
+
# @param project [Hash] the project to parse
|
14
|
+
def initialize project
|
15
|
+
@project = project
|
16
|
+
@objects = @project[Keys::OBJECTS]
|
17
|
+
@visited = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Recursively traverse `object` building the `visited` Hash by keeping track of the current path
|
21
|
+
#
|
22
|
+
# Objects that have already been visited are skipped to avoid cycles or modifying existing values
|
23
|
+
#
|
24
|
+
# @param object [Hash, Array, String] the current object to traverse
|
25
|
+
# @param path [String] the current path to this node
|
26
|
+
def parse object:, path: ""
|
27
|
+
return if visited.has_key? object
|
28
|
+
|
29
|
+
case object
|
30
|
+
when Array
|
31
|
+
object.each do |value|
|
32
|
+
parse object: value, path: path
|
33
|
+
end
|
34
|
+
when Hash
|
35
|
+
parse object: object.values, path: path
|
36
|
+
when String
|
37
|
+
current = objects[object]
|
38
|
+
|
39
|
+
return if current.nil? || current[Keys::ISA].nil?
|
40
|
+
|
41
|
+
path += '/' + current[Keys::ISA] + Helpers.resolve_attributes(object, objects)
|
42
|
+
|
43
|
+
visited[object] = path
|
44
|
+
|
45
|
+
parse object: current, path: path
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_accessor :objects, :project
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|