xcunique 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +22 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +73 -0
  7. data/Rakefile +11 -0
  8. data/bin/xcunique +5 -0
  9. data/lib/json2plist/json2plist +21 -0
  10. data/lib/xcunique.rb +23 -0
  11. data/lib/xcunique/cli.rb +40 -0
  12. data/lib/xcunique/hash_ext.rb +25 -0
  13. data/lib/xcunique/helpers.rb +26 -0
  14. data/lib/xcunique/options.rb +68 -0
  15. data/lib/xcunique/parser.rb +54 -0
  16. data/lib/xcunique/sorter.rb +51 -0
  17. data/lib/xcunique/uniquifier.rb +36 -0
  18. data/lib/xcunique/version.rb +3 -0
  19. data/spec/fixtures/TestProject.json +548 -0
  20. data/spec/fixtures/TestProject/TestProject.xcodeproj/project.pbxproj +510 -0
  21. data/spec/fixtures/TestProject/TestProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  22. data/spec/fixtures/TestProject/TestProject.xcodeproj/project.xcworkspace/xcuserdata/paul.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  23. data/spec/fixtures/TestProject/TestProject.xcodeproj/xcuserdata/paul.xcuserdatad/xcschemes/TestProject.xcscheme +111 -0
  24. data/spec/fixtures/TestProject/TestProject.xcodeproj/xcuserdata/paul.xcuserdatad/xcschemes/xcschememanagement.plist +47 -0
  25. data/spec/fixtures/TestProject/TestProject/AppDelegate.swift +46 -0
  26. data/spec/fixtures/TestProject/TestProject/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  27. data/spec/fixtures/TestProject/TestProject/Base.lproj/LaunchScreen.storyboard +27 -0
  28. data/spec/fixtures/TestProject/TestProject/Base.lproj/Main.storyboard +26 -0
  29. data/spec/fixtures/TestProject/TestProject/Info.plist +47 -0
  30. data/spec/fixtures/TestProject/TestProject/ViewController.swift +25 -0
  31. data/spec/fixtures/TestProject/TestProjectTests/Info.plist +24 -0
  32. data/spec/fixtures/TestProject/TestProjectTests/TestProjectTests.swift +36 -0
  33. data/spec/fixtures/TestProject/TestProjectUITests/Info.plist +24 -0
  34. data/spec/fixtures/TestProject/TestProjectUITests/TestProjectUITests.swift +36 -0
  35. data/spec/spec_helper.rb +4 -0
  36. data/spec/xcunique/hash_ext_spec.rb +36 -0
  37. data/spec/xcunique/helpers_spec.rb +58 -0
  38. data/spec/xcunique/parser_spec.rb +214 -0
  39. data/spec/xcunique/sorter_spec.rb +116 -0
  40. data/xcunique.gemspec +26 -0
  41. metadata +178 -0
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xcunique.gemspec
4
+ gemspec
@@ -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
@@ -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.
@@ -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
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['spec/**/*_spec.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ desc "Run tests"
11
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'xcunique'
4
+
5
+ Xcunique::CLI.run(ARGV)
@@ -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
+ }
@@ -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"
@@ -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