Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Introduce Xcodeproj::Differ

  • Loading branch information...
commit 308941eeaa3bca817742c774fd584cc5ab1c8f84 1 parent 47d91e8
@fabiopelosin fabiopelosin authored
View
5 lib/xcodeproj.rb
@@ -10,11 +10,12 @@ def message
end
end
- autoload :Config, 'xcodeproj/config'
autoload :Command, 'xcodeproj/command'
+ autoload :Config, 'xcodeproj/config'
autoload :Constants, 'xcodeproj/constants'
+ autoload :Differ, 'xcodeproj/differ'
autoload :Helper, 'xcodeproj/helper'
autoload :Project, 'xcodeproj/project'
- autoload :Workspace, 'xcodeproj/workspace'
autoload :UI, 'xcodeproj/user_interface'
+ autoload :Workspace, 'xcodeproj/workspace'
end
View
14 lib/xcodeproj/command/project_diff.rb
@@ -32,20 +32,20 @@ def initialize(argv)
def run
- hash_1 = Project.new(@path_project1).to_tree_hash
- hash_2 = Project.new(@path_project2).to_tree_hash
- (@keys_to_ignore).each do |key|
- hash_1.recursive_delete(key)
- hash_2.recursive_delete(key)
+ hash_1 = Project.new(@path_project1).to_tree_hash.dup
+ hash_2 = Project.new(@path_project2).to_tree_hash.dup
+ @keys_to_ignore.each do |key|
+ Differ.clean_hash!(hash_1, key)
+ Differ.clean_hash!(hash_2, key)
end
- diff = hash_1.recursive_diff(hash_2, @path_project1, @path_project2)
- diff.recursive_delete('displayName')
+ diff = Differ.project_diff(hash_1, hash_2, @path_project1, @path_project2)
require 'yaml'
yaml = diff.to_yaml
yaml = yaml.gsub(@path_project1, @path_project1.cyan)
yaml = yaml.gsub(@path_project2, @path_project2.magenta)
+ yaml = yaml.gsub(":diff:", "diff:".yellow)
puts yaml
end
end
View
268 lib/xcodeproj/differ.rb
@@ -0,0 +1,268 @@
+module Xcodeproj
+
+ # Computes the recursive diff of Hashes, Array and other objects.
+ #
+ # Useful to compare two projects. Inspired from
+ # 'active_support/core_ext/hash/diff'.
+ #
+ # @example
+ # h1 = { :common => 'value', :changed => 'v1' }
+ # h2 = { :common => 'value', :changed => 'v2', :addition => 'new_value' }
+ # h1.recursive_diff(h2) == {
+ # :changed => {
+ # :self => 'v1',
+ # :other => 'v2'
+ # },
+ # :addition => {
+ # :self => nil,
+ # :other => 'new_value'
+ # }
+ # } #=> true
+ #
+ #
+ #
+ #
+ module Differ
+
+ # Computes the recursive difference of two given values.
+ #
+ # @param [Object] value_1
+ # The first value to compare.
+ #
+ # @param [Object] value_2
+ # The second value to compare.
+ #
+ # @param [Object] key_1
+ # The key for the diff of value_1.
+ #
+ # @param [Object] key_2
+ # The key for the diff of value_2.
+ #
+ # @param [Object] id_key
+ # The key used to identify correspondent hashes in an array.
+ #
+ # @return [Hash] The diff
+ # @return [Nil] if the given values are equal.
+ #
+ def self.diff(value_1, value_2, options = {})
+ options[:key_1] ||= 'value_1'
+ options[:key_2] ||= 'value_2'
+ options[:id_key] ||= nil
+
+ if value_1.class == value_2.class
+ method = case value_1
+ when Hash then :hash_diff
+ when Array then :array_diff
+ else :generic_diff
+ end
+ else
+ method = :generic_diff
+ end
+ self.send(method, value_1, value_2, options)
+ end
+
+ # Optimized for reducing the noise from the tree hash of projects
+ #
+ def self.project_diff(project_1, project_2, key_1 = 'project_1', key_2 = 'project_2')
+ project_1 = project_1.to_tree_hash unless project_1.is_a?(Hash)
+ project_2 = project_2.to_tree_hash unless project_2.is_a?(Hash)
+ options = {
+ :key_1 => key_1,
+ :key_2 => key_2,
+ :id_key => 'displayName',
+ }
+ diff(project_1, project_2, options)
+ end
+
+ #-------------------------------------------------------------------------#
+
+ public
+
+ # @!group Type specific handlers
+
+ # Computes the recursive difference of two hashes.
+ #
+ # @see diff
+ #
+ def self.hash_diff(value_1, value_2, options)
+ ensure_class(value_1, Hash)
+ ensure_class(value_2, Hash)
+ return nil if value_1 == value_2
+
+ result = {}
+ all_keys = (value_1.keys + value_2.keys).uniq
+ all_keys.each do |key|
+ key_value_1 = value_1[key]
+ key_value_2 = value_2[key]
+ diff = diff(key_value_1, key_value_2, options)
+ if diff
+ result[key] = diff if diff
+ end
+ end
+ if result.empty?
+ nil
+ else
+ result
+ end
+ end
+
+ # Returns the recursive diff of two arrays.
+ #
+ # @see diff
+ #
+ def self.array_diff(value_1, value_2, options)
+ ensure_class(value_1, Array)
+ ensure_class(value_2, Array)
+ return nil if value_1 == value_2
+
+ new_objects_value_1 = (value_1 - value_2)
+ new_objects_value_2 = (value_2 - value_1)
+ return nil if value_1.empty? && value_2.empty?
+
+ matched_diff = {}
+ if id_key = options[:id_key]
+ matched_value_1 = []
+ matched_value_2 = []
+ new_objects_value_1.each do |entry_value_1|
+ if entry_value_1.is_a?(Hash)
+ id_value = entry_value_1[id_key]
+ entry_value_2 = new_objects_value_2.find do |entry|
+ entry[id_key] == id_value
+ end
+ if entry_value_2
+ matched_value_1 << entry_value_1
+ matched_value_2 << entry_value_2
+ diff = diff(entry_value_1, entry_value_2, options)
+ matched_diff[id_value] = diff if diff
+ end
+ end
+ end
+
+ new_objects_value_1 = new_objects_value_1 - matched_value_1
+ new_objects_value_2 = new_objects_value_2 - matched_value_2
+ end
+
+ if new_objects_value_1.empty? && new_objects_value_2.empty?
+ if matched_diff.empty?
+ nil
+ else
+ matched_diff
+ end
+ else
+ result = {}
+ result[options[:key_1]] = new_objects_value_1 unless new_objects_value_1.empty?
+ result[options[:key_2]] = new_objects_value_2 unless new_objects_value_2.empty?
+ result[:diff] = matched_diff unless matched_diff.empty?
+ result
+ end
+ end
+
+ # Returns the diff of two generic objects.
+ #
+ # @see diff
+ #
+ def self.generic_diff(value_1, value_2, options)
+ return nil if value_1 == value_2
+
+ {
+ options[:key_1] => value_1,
+ options[:key_2] => value_2
+ }
+ end
+
+ #-------------------------------------------------------------------------#
+
+ public
+
+ # @!group Cleaning
+
+ # Returns a copy of the hash where the given key is removed recursively.
+ #
+ # @param [Hash] hash
+ # The hash to clean
+ #
+ # @param [Object] key
+ # The key to remove.
+ #
+ # @return [Hash] A copy of the hash without the key.
+ #
+ def self.clean_hash(hash, key)
+ new_hash = hash.dup
+ self.clean_hash!(new_hash, key)
+ new_hash
+ end
+
+ # Recursively cleans a key from the given hash.
+ #
+ # @param [Hash] hash
+ # The hash to clean
+ #
+ # @param [Object] key
+ # The key to remove.
+ #
+ # @return [void]
+ #
+ def self.clean_hash!(hash, key)
+ hash.delete(key)
+ hash.each do |_, value|
+ case value
+ when Hash
+ clean_hash!(value, key)
+ when Array
+ value.each { |entry| clean_hash!(entry, key) if entry.is_a?(Hash)}
+ end
+ end
+ end
+
+ #-------------------------------------------------------------------------#
+
+ private
+
+ # @! Helpers
+
+ # Ensures that the given object belongs to the given class.
+ #
+ # @param [Object] object
+ # The object to check.
+ #
+ # @param [Class] klass
+ # the expected class of the object.
+ #
+ # @raise If the object doesn't belong to the given class.
+ #
+ # @return [void]
+ #
+ def self.ensure_class(object, klass)
+ raise "Wrong type `#{object.inspect}`" unless object.is_a?(klass)
+ end
+
+ #-------------------------------------------------------------------------#
+
+ end
+end
+
+#-----------------------------------------------------------------------------#
+
+# TODO: Remove monkey patching
+
+class Hash
+ def recursive_diff(other, self_key = 'self', other_key = 'other')
+ Xcodeproj::Differ.project_diff(self, other, self_key, other_key)
+ end
+
+ def recursive_delete(key_to_delete)
+ Xcodeproj::Differ.project_diff!(self, key_to_delete)
+ end
+end
+
+class Array
+ def recursive_diff(other, self_key = 'self', other_key = 'other')
+ Xcodeproj::Differ.project_diff(self, other, self_key, other_key)
+ end
+end
+
+class Object
+ def recursive_diff(other, self_key = 'self', other_key = 'other')
+ Xcodeproj::Differ.project_diff(self, other, self_key, other_key)
+ end
+end
View
2  lib/xcodeproj/project.rb
@@ -1,9 +1,7 @@
require 'fileutils'
require 'pathname'
require 'xcodeproj/xcodeproj_ext'
-
require 'xcodeproj/project/object'
-require 'xcodeproj/project/recursive_diff'
module Xcodeproj
View
116 lib/xcodeproj/project/recursive_diff.rb
@@ -1,116 +0,0 @@
-class Hash
-
- # Computes the recursive difference of two hashes.
- #
- # Useful to compare two projects.
- #
- # Inspired from 'active_support/core_ext/hash/diff'.
- #
- # @example
- # h1 = { :common => 'value', :changed => 'v1' }
- # h2 = { :common => 'value', :changed => 'v2', :addition => 'new_value' }
-
- # h1.recursive_diff(h2) == {
- # :changed => {
- # :self => 'v1',
- # :other => 'v2'
- # },
- # :addition => {
- # :self => nil,
- # :other => 'new_value'
- # }
- # } #=> true
- #
- # @return [Hash] Returns the recursive difference of a hash.
- #
- def recursive_diff(other, self_key = 'self', other_key = 'other')
- if other.is_a?(Hash)
- r = {}
- all_keys = self.keys + other.keys
- all_keys.each do |key|
- v1 = self[key]
- v2 = other[key]
- diff = v1.recursive_diff(v2, self_key, other_key)
- r[key] = diff if diff
- end
- r unless r == {}
- else
- super
- end
- end
-
- # @return [void]
- #
- def recursive_delete(key_to_delete)
- delete(key_to_delete)
- self.each do |key, value|
- case value
- when Hash
- value.recursive_delete(key_to_delete)
- when Array
- value.each { |v| v.recursive_delete(key_to_delete) if v.is_a?(Hash)}
- end
- end
- end
-end
-
-
-class Array
-
- # @return [Array]
- #
- def recursive_diff(other, self_key = 'self', other_key = 'other')
- if other.is_a?(Array)
- new_objects_self = (self - other)
- new_objects_other = (other - self)
- unmatched_objects_self = []
- array_result = []
-
- # Try to match objects to reduce noise
- new_objects_self.each do |value|
- if value.is_a?(Hash)
- other_value = new_objects_other.find do |other|
- other.is_a?(Hash) && (value['displayName'] == other['displayName'])
- end
-
- if other_value
- new_objects_other.delete(other_value)
- match_diff = value.recursive_diff(other_value, self_key, other_key)
- array_result << { value['displayName'] => match_diff} unless match_diff == {}
- else
- unmatched_objects_self << value
- end
- end
- end
-
- unless unmatched_objects_self.empty?
- array_result << {
- self_key => unmatched_objects_self.map do |v|
- { v['displayName'] => v }
- end
- }
- end
-
- unless new_objects_other.empty?
- array_result << {
- other_key => new_objects_other.map do |v|
- { v['displayName'] => v }
- end
- }
- end
-
- array_result unless array_result == []
- else
- super
- end
- end
-end
-
-class Object
-
- # @return [Hash]
- #
- def recursive_diff(other, self_key = 'self', other_key = 'other')
- { self_key => self, other_key => other } unless self == other
- end
-end
View
245 spec/differ_spec.rb
@@ -0,0 +1,245 @@
+require File.expand_path('../spec_helper', __FILE__)
+
+def options
+ {
+ :key_1 => 'v1',
+ :key_2 => 'v2',
+ }
+end
+
+module Xcodeproj
+
+ describe Differ do
+
+ describe "Hashes" do
+
+ it "returns the nil if the hashes are equal" do
+ v1 = { :add => 'add' }
+ diff = Differ.hash_diff(v1, v1, options)
+ diff.should == nil
+ end
+
+ it "returns the whether a key was added to the first array" do
+ v1 = { :add => 'add' }
+ v2 = { }
+ diff = Differ.hash_diff(v1, v2, options)
+ diff.should == { :add => { "v1" => "add" , "v2" => nil } }
+ end
+
+ it "returns the whether a key was added to the second array" do
+ v1 = { }
+ v2 = { :add => 'add' }
+ diff = Differ.hash_diff(v1, v2, options)
+ diff.should == { :add => { "v1" => nil , "v2" => "add" } }
+ end
+
+ it "returns the whether the value of a key has changed" do
+ v1 = { :key => '123' }
+ v2 = { :key => '456' }
+ diff = Differ.hash_diff(v1, v2, options)
+ diff.should == { :key => { "v1" => "123" , "v2" => "456" } }
+ end
+
+ it "handles keys which contain arrays" do
+ v1 = { :key => [1, 2, 3] }
+ v2 = { :key => [1, 2, 4] }
+ diff = Differ.hash_diff(v1, v2, options)
+ diff.should == {:key=>{"v1"=>[3], "v2"=>[4]}}
+ end
+
+ it "handles nested arrays" do
+ v1 = { :key => { :subvalue_1 => { :entry_1 => 'A' }, :subvalue_2 => { :entry_1 => 'A' } }}
+ v2 = { :key => { :subvalue_1 => { :entry_1 => 'A' }, :subvalue_2 => { :entry_1 => 'B' } }}
+ diff = Differ.hash_diff(v1, v2, options)
+ diff.should == {:key=>{:subvalue_2=>{:entry_1=>{"v1"=>"A", "v2"=>"B"}}}}
+ end
+ end
+
+ #-------------------------------------------------------------------------#
+
+ describe "Arrays" do
+
+ it "returns the nil if the arrays are equal" do
+ v1 = [1, 2, 3]
+ diff = Differ.array_diff(v1, v1, options)
+ diff.should == nil
+ end
+
+ it "returns the diff of two arrays" do
+ v1 = [1, 2, 3]
+ v2 = [1, 2, 4]
+ diff = Differ.array_diff(v1, v2, options)
+ diff.should == { "v1" => [3], "v2"=>[4] }
+ end
+
+ it "returns the diff of two arrays containing an hash" do
+ v1 = [{ :key => 'value_1' }]
+ v2 = [{ :key => 'value_2' }]
+ diff = Differ.array_diff(v1, v2, options)
+ diff.should == {
+ "v1" => [ {:key=>"value_1"} ],
+ "v2" => [ {:key=>"value_2"} ]
+ }
+ end
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ describe "Generic diff" do
+
+ it "returns nil as the diff of two equal objects" do
+ v1 = "String"
+ v2 = "String"
+ diff = Differ.generic_diff(v1, v2, options)
+ diff.should.be.nil
+ end
+
+ it "returns the diff of two objects which do not represent a collection" do
+ v1 = "String_1"
+ v2 = "String_2"
+ diff = Differ.generic_diff(v1, v2, options)
+ diff.should == { "v1" => "String_1", "v2" => "String_2" }
+ end
+
+ it "handles the case where one of the values is a collection object" do
+ v1 = [ "String_1" ]
+ v2 = "String_2"
+ diff = Differ.generic_diff(v1, v2, options)
+ diff.should == { "v1" => ["String_1"], "v2" => "String_2" }
+ end
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ describe "Cleaning" do
+
+ it "cleans an hash from the given key" do
+ hash = { :key => 'v1', :delete => 'v2' }
+ Differ.clean_hash!(hash, :delete)
+ hash.should == { :key => 'v1' }
+ end
+
+ it "cleans an hash from the given key non destructively" do
+ hash = { :key => 'v1', :delete => 'v2' }
+ clean = Differ.clean_hash(hash, :delete)
+ clean.should == { :key => 'v1' }
+ hash.should == { :key => 'v1', :delete => 'v2' }
+ end
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ describe "#diff" do
+
+ it "returns nil as the diff of two equal objects" do
+ v1 = { :key => ["String"] }
+ v2 = { :key => ["String"] }
+ diff = Differ.diff(v1, v2, options)
+ diff.should.be.nil
+ end
+
+ it "handles hashes" do
+ v1 = { :key => "value_1" }
+ v2 = { :key => "value_2" }
+ diff = Differ.diff(v1, v2, options)
+ diff.should == {:key=>{"v1"=>"value_1", "v2"=>"value_2"}}
+ end
+
+ it "handles arrays" do
+ v1 = ["value_1"]
+ v2 = ["value_2"]
+ diff = Differ.diff(v1, v2, options)
+ diff.should == {"v1"=>["value_1"], "v2"=>["value_2"]}
+ end
+
+ it "handles generic objects" do
+ v1 = "value_1"
+ v2 = "value_2"
+ diff = Differ.diff(v1, v2, options)
+ diff.should == {"v1"=>"value_1", "v2"=>"value_2"}
+ end
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ describe "#project_diff" do
+
+ it "provides a succint diff of a project" do
+ project_1 = {
+ "mainGroup" => {
+ "displayName"=>"Main Group", "isa"=>"PBXGroup", "sourceTree"=>"<group>",
+ "children" => [
+ {"displayName"=>"A Group", "isa"=>"PBXGroup", "sourceTree"=>"<group>", "name"=>"Products", "children"=>
+ [
+ {"displayName"=>"file_1.m", "isa"=>"PBXFileReference", "path" => "path/file_1.m"}
+ ]
+ },
+ ]
+ },
+ }
+
+ project_2 = {
+ "mainGroup" => {
+ "displayName"=>"Main Group", "isa"=>"PBXGroup", "sourceTree"=>"<group>",
+ "children" => [
+ {"displayName"=>"A Group", "isa"=>"PBXGroup", "sourceTree"=>"<group>", "name"=>"Products", "children"=>
+ [
+ {"displayName"=>"file_1.m", "isa"=>"PBXFileReference", "path" => "new_path/file_1.m"}
+ ]
+ },
+ ]
+ },
+ }
+
+ diff = Differ.project_diff(project_1, project_2)
+ diff.should == {
+ "mainGroup" => {
+ "children" => {
+ "A Group" => {
+ "children" => {
+ "file_1.m" => {
+ "path" => {
+ "project_1"=>"path/file_1.m",
+ "project_2"=>"new_path/file_1.m"}
+ }
+ }
+ }
+ }
+ }
+ }
+ end
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ end
+
+ #-------------------------------------------------------------------------#
+
+ # TODO: delete
+ describe Hash do
+
+ it "returns the recursive diff with another hash" do
+ v1 = { :common => 'value', :changed => 'v1' }
+ v2 = { :common => 'value', :changed => 'v2', :addition => 'new_value' }
+
+ v1.recursive_diff(v2).should == {
+ :changed => {
+ "self" => 'v1',
+ "other" => 'v2'
+ },
+ :addition => {
+ "self" => nil,
+ "other" => 'new_value'
+ }
+ }
+ end
+
+ end
+end
+
View
24 spec/project/recursive_diff_spec.rb
@@ -1,24 +0,0 @@
-require File.expand_path('../../spec_helper', __FILE__)
-
-module ProjectSpecs
-
- describe Hash do
-
- it "returns the recursive diff with another hash" do
- h1 = { :common => 'value', :changed => 'v1' }
- h2 = { :common => 'value', :changed => 'v2', :addition => 'new_value' }
-
- h1.recursive_diff(h2).should == {
- :changed => {
- "self" => 'v1',
- "other" => 'v2'
- },
- :addition => {
- "self" => nil,
- "other" => 'new_value'
- }
- }
- end
-
- end
-end
View
20 spec/project_spec.rb
@@ -88,10 +88,8 @@ module ProjectSpecs
it "can regenerate the EXACT plist that initialized it" do
plist = Xcodeproj.read_plist(fixture_path("Sample Project/Cocoa Application.xcodeproj/project.pbxproj"))
generated = @project.to_plist
- diff = generated.recursive_diff(plist, "generated", "plist")
+ diff = Xcodeproj::Differ.diff(generated, plist)
diff.should.be.nil
- # The diff is there for readability of errors
- generated.should == plist
end
it "doesn't add default attributes to objects generated from a plist" do
@@ -246,13 +244,17 @@ module ProjectSpecs
end
it "returns a succinct diff representation of the project" do
- before_tree_hash = @project.to_tree_hash
+ before_proj = @project.to_tree_hash
@project.new_group('Pods')
- diff = @project.to_tree_hash.recursive_diff(before_tree_hash)
- diff.should == {"rootObject"=>{"mainGroup"=>{"children"=>[{"self"=>[{
- "Pods"=>{
- "displayName"=>"Pods", "isa"=>"PBXGroup", "sourceTree"=>"<group>", "name"=>"Pods", "children"=>[]}
- }]}]}}}
+ after_proj = @project.to_tree_hash
+ diff = Xcodeproj::Differ.project_diff(before_proj, after_proj)
+
+ diff.should == {
+ "rootObject"=>{"mainGroup"=>{"children"=>{
+ "project_2"=>[
+ {"displayName"=>"Pods", "isa"=>"PBXGroup", "sourceTree"=>"<group>", "name"=>"Pods", "children"=>[]}
+ ]
+ }}}}
end
it "returns a pretty print representation" do
Please sign in to comment.
Something went wrong with that request. Please try again.