kintsugi 0.1.1 → 0.4.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/.rubocop.yml +3 -0
- data/README.md +33 -1
- data/bin/kintsugi +23 -36
- data/kintsugi.gemspec +1 -1
- data/lib/kintsugi/apply_change_to_project.rb +77 -39
- data/lib/kintsugi/cli.rb +203 -0
- data/lib/kintsugi/error.rb +9 -0
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi.rb +77 -29
- data/spec/kintsugi_apply_change_to_project_spec.rb +114 -4
- metadata +14 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 0e2593fa95851c928b7ad943430c1db40d4c21831e24bd16f371f24298714c99
         | 
| 4 | 
            +
              data.tar.gz: dca49c53301790803690ad5aa81dcf59b2f9036402a713b63b9093130994fe51
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: aa8c40252da2e56f9753fd143e1b100599c31737badcb5c16906e5883008b6344a5e44c09f2619f90045b0c987971f0248f2400d5f95acd20dac7c810b061a05
         | 
| 7 | 
            +
              data.tar.gz: 15da61db3bea0d5d3c9528ba93b087ae67956eb58f4d846639936fcf682ac5f7cc5f6d2ec8eb87cc41673b960978e245b3eaf33d21b31c1e5a0ebadf46780d07
         | 
    
        data/.rubocop.yml
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -38,13 +38,45 @@ When there's a `.pbxproj` file with Git conflicts, and a 3-way merge is possible | |
| 38 38 |  | 
| 39 39 | 
             
            And see the magic happen! :sparkles:
         | 
| 40 40 |  | 
| 41 | 
            +
            ### Git merge driver
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            You can setup Kintsugi to automatically resolve conflicts that occur in `pbxproj` files when such conflicts occur.
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            #### Automatic install
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            Run `kintsugi install-driver`. This will install Kintsugi as a merge driver globally. Note that Kintsugi needs to be in your `PATH`.
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            ❗ Do not install with bundler because the installation might succeed even if Kintsugi is not in `PATH`.
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            #### Manual install
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            - Add Kintsugi as driver to Git config file by running the following:
         | 
| 54 | 
            +
            ```sh
         | 
| 55 | 
            +
            git config merge.kintsugi.name "Kintsugi driver" # Or any other name you prefer
         | 
| 56 | 
            +
            git config merge.kintsugi.driver "<path_to_kintsugi> driver %O %A %B %P"
         | 
| 57 | 
            +
            ```
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            Run `git config` with `--global` to add this to the global config file.
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            - Add the following line to the `.gitattributes` file at the root of the repository:
         | 
| 62 | 
            +
             | 
| 63 | 
            +
            `*.pbxproj merge=kintsugi`
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            This will instruct Git to use Kintsugi as a merge driver for `.pbxproj` files.
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            See the [official docs](https://git-scm.com/docs/gitattributes) if you want to set this globally.
         | 
| 68 | 
            +
             | 
| 41 69 | 
             
            ## Contribution
         | 
| 42 70 |  | 
| 43 71 | 
             
            See our [Contribution guidelines](./CONTRIBUTING.md).
         | 
| 44 72 |  | 
| 45 73 | 
             
            ## Alternatives
         | 
| 46 74 |  | 
| 47 | 
            -
             | 
| 75 | 
            +
            All of the alternatives below allow you to generate your Xcode projects based on a spec or manifest. You commit these files to git, and can even remove the `.xcodeproj` files from git.
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            - [XcodeGen](https://github.com/yonaskolb/XcodeGen)
         | 
| 78 | 
            +
            - [Tuist](https://github.com/tuist)
         | 
| 79 | 
            +
            - [Xcake](https://github.com/igor-makarov/xcake)
         | 
| 48 80 |  | 
| 49 81 | 
             
            ## Copyright
         | 
| 50 82 |  | 
    
        data/bin/kintsugi
    CHANGED
    
    | @@ -4,49 +4,36 @@ | |
| 4 4 | 
             
            # Copyright (c) 2020 Lightricks. All rights reserved.
         | 
| 5 5 | 
             
            # Created by Ben Yohay.
         | 
| 6 6 |  | 
| 7 | 
            -
            require "json"
         | 
| 8 | 
            -
            require "optparse"
         | 
| 9 | 
            -
             | 
| 10 7 | 
             
            require "kintsugi"
         | 
| 11 | 
            -
            require_relative "../lib/kintsugi/ | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
            def parse_options!(argv)
         | 
| 15 | 
            -
              options_parser = create_options_parser
         | 
| 8 | 
            +
            require_relative "../lib/kintsugi/cli"
         | 
| 9 | 
            +
            require_relative "../lib/kintsugi/error"
         | 
| 16 10 |  | 
| 11 | 
            +
            def parse_options!(command, argv)
         | 
| 17 12 | 
             
              options = {}
         | 
| 18 | 
            -
               | 
| 19 | 
            -
             | 
| 20 | 
            -
              if argv.length != 1
         | 
| 21 | 
            -
                puts "Incorrect number of arguments\n\n"
         | 
| 22 | 
            -
                puts options_parser
         | 
| 23 | 
            -
                exit(1)
         | 
| 24 | 
            -
              end
         | 
| 25 | 
            -
             | 
| 13 | 
            +
              command.option_parser.parse!(argv, into: options)
         | 
| 26 14 | 
             
              options
         | 
| 27 15 | 
             
            end
         | 
| 28 16 |  | 
| 29 | 
            -
            def  | 
| 30 | 
            -
               | 
| 31 | 
            -
             | 
| 32 | 
            -
                              "Lightricks\n\nUsage: kintsugi [pbxproj_filepath] [options]"
         | 
| 33 | 
            -
                opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
         | 
| 34 | 
            -
                        "written in JSON format. Used for debug purposes.")
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                opts.on("-h", "--help", "Prints this help") do
         | 
| 37 | 
            -
                  puts opts
         | 
| 38 | 
            -
                  exit
         | 
| 39 | 
            -
                end
         | 
| 17 | 
            +
            def name_of_subcommand?(subcommands, argument)
         | 
| 18 | 
            +
              subcommands.include?(argument)
         | 
| 19 | 
            +
            end
         | 
| 40 20 |  | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 21 | 
            +
            first_argument = ARGV[0]
         | 
| 22 | 
            +
            cli = Kintsugi::CLI.new
         | 
| 23 | 
            +
            command =
         | 
| 24 | 
            +
              if name_of_subcommand?(cli.subcommands, first_argument)
         | 
| 25 | 
            +
                ARGV.shift
         | 
| 26 | 
            +
                cli.subcommands[first_argument]
         | 
| 27 | 
            +
              else
         | 
| 28 | 
            +
                cli.root_command
         | 
| 45 29 | 
             
              end
         | 
| 46 | 
            -
            end
         | 
| 47 30 |  | 
| 48 | 
            -
            options = parse_options!(ARGV)
         | 
| 49 | 
            -
            project_file_path = File.expand_path(ARGV[0])
         | 
| 50 | 
            -
            Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
         | 
| 31 | 
            +
            options = parse_options!(command, ARGV)
         | 
| 51 32 |  | 
| 52 | 
            -
             | 
| 33 | 
            +
            begin
         | 
| 34 | 
            +
              command.action.call(options, ARGV)
         | 
| 35 | 
            +
            rescue ArgumentError => e
         | 
| 36 | 
            +
              puts "#{e.class}: #{e}"
         | 
| 37 | 
            +
            rescue Kintsugi::MergeError => e
         | 
| 38 | 
            +
              puts e
         | 
| 39 | 
            +
            end
         | 
    
        data/kintsugi.gemspec
    CHANGED
    
    | @@ -22,7 +22,7 @@ Gem::Specification.new do |spec| | |
| 22 22 | 
             
              spec.test_files    = spec.files.grep(%r{^(spec)/})
         | 
| 23 23 | 
             
              spec.require_paths = ["lib"]
         | 
| 24 24 |  | 
| 25 | 
            -
              spec.add_dependency "xcodeproj", "1.19.0"
         | 
| 25 | 
            +
              spec.add_dependency "xcodeproj", ">= 1.19.0", "<= 1.21.0"
         | 
| 26 26 |  | 
| 27 27 | 
             
              spec.add_development_dependency "rake", "~> 13.0"
         | 
| 28 28 | 
             
              spec.add_development_dependency "rspec", "~> 3.9"
         | 
| @@ -167,7 +167,7 @@ module Kintsugi | |
| 167 167 | 
             
                  new_value = nil
         | 
| 168 168 |  | 
| 169 169 | 
             
                  if change.key?(:removed)
         | 
| 170 | 
            -
                    new_value = apply_removal_to_simple_attribute(old_value, change[:removed])
         | 
| 170 | 
            +
                    new_value = apply_removal_to_simple_attribute(old_value, change[:removed], change[:added])
         | 
| 171 171 | 
             
                  end
         | 
| 172 172 |  | 
| 173 173 | 
             
                  if change.key?(:added)
         | 
| @@ -183,29 +183,31 @@ module Kintsugi | |
| 183 183 | 
             
                  new_value
         | 
| 184 184 | 
             
                end
         | 
| 185 185 |  | 
| 186 | 
            -
                def apply_removal_to_simple_attribute(old_value,  | 
| 187 | 
            -
                  case  | 
| 186 | 
            +
                def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
         | 
| 187 | 
            +
                  case removed_change
         | 
| 188 188 | 
             
                  when Array
         | 
| 189 | 
            -
                    (old_value || []) -  | 
| 189 | 
            +
                    (old_value || []) - removed_change
         | 
| 190 190 | 
             
                  when Hash
         | 
| 191 191 | 
             
                    (old_value || {}).reject do |key, value|
         | 
| 192 | 
            -
                      if value !=  | 
| 193 | 
            -
                        raise "Trying to remove value #{ | 
| 194 | 
            -
                          "to #{value}."
         | 
| 192 | 
            +
                      if value != removed_change[key] && added_change[key] != value
         | 
| 193 | 
            +
                        raise MergeError, "Trying to remove value '#{removed_change[key]}' of hash with key " \
         | 
| 194 | 
            +
                          "'#{key}' but it changed to #{value}. This is considered a conflict that should be " \
         | 
| 195 | 
            +
                          "resolved manually."
         | 
| 195 196 | 
             
                      end
         | 
| 196 197 |  | 
| 197 | 
            -
                       | 
| 198 | 
            +
                      removed_change.key?(key)
         | 
| 198 199 | 
             
                    end
         | 
| 199 200 | 
             
                  when String
         | 
| 200 | 
            -
                    if old_value !=  | 
| 201 | 
            -
                      raise " | 
| 201 | 
            +
                    if old_value != removed_change && !old_value.nil? && added_change != old_value
         | 
| 202 | 
            +
                      raise MergeError, "Trying to remove value '#{removed_change}', but the existing value " \
         | 
| 203 | 
            +
                        "is '#{old_value}'. This is considered a conflict that should be resolved manually."
         | 
| 202 204 | 
             
                    end
         | 
| 203 205 |  | 
| 204 206 | 
             
                    nil
         | 
| 205 207 | 
             
                  when nil
         | 
| 206 208 | 
             
                    nil
         | 
| 207 209 | 
             
                  else
         | 
| 208 | 
            -
                    raise "Unsupported change #{ | 
| 210 | 
            +
                    raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
         | 
| 209 211 | 
             
                  end
         | 
| 210 212 | 
             
                end
         | 
| 211 213 |  | 
| @@ -218,7 +220,8 @@ module Kintsugi | |
| 218 220 | 
             
                    new_value = old_value.merge(change)
         | 
| 219 221 |  | 
| 220 222 | 
             
                    unless (old_value.to_a - new_value.to_a).empty?
         | 
| 221 | 
            -
                      raise "New hash #{change} contains values that conflict with old hash  | 
| 223 | 
            +
                      raise MergeError, "New hash #{change} contains values that conflict with old hash " \
         | 
| 224 | 
            +
                        "#{old_value}"
         | 
| 222 225 | 
             
                    end
         | 
| 223 226 |  | 
| 224 227 | 
             
                    new_value
         | 
| @@ -227,14 +230,15 @@ module Kintsugi | |
| 227 230 | 
             
                  when nil
         | 
| 228 231 | 
             
                    nil
         | 
| 229 232 | 
             
                  else
         | 
| 230 | 
            -
                    raise "Unsupported change #{change} of type #{change.class}"
         | 
| 233 | 
            +
                    raise MergeError, "Unsupported change #{change} of type #{change.class}"
         | 
| 231 234 | 
             
                  end
         | 
| 232 235 | 
             
                end
         | 
| 233 236 |  | 
| 234 237 | 
             
                def remove_component(component, change)
         | 
| 235 238 | 
             
                  if component.to_tree_hash != change
         | 
| 236 | 
            -
                    raise "Trying to remove an object that changed since then. This is  | 
| 237 | 
            -
                      "that should be resolved manually. Name of the object is:  | 
| 239 | 
            +
                    raise MergeError, "Trying to remove an object that changed since then. This is " \
         | 
| 240 | 
            +
                      "considered a conflict that should be resolved manually. Name of the object is: " \
         | 
| 241 | 
            +
                      "'#{component.display_name}'"
         | 
| 238 242 | 
             
                  end
         | 
| 239 243 |  | 
| 240 244 | 
             
                  if change["isa"] == "PBXFileReference"
         | 
| @@ -265,6 +269,8 @@ module Kintsugi | |
| 265 269 | 
             
                  case change["isa"]
         | 
| 266 270 | 
             
                  when "PBXNativeTarget"
         | 
| 267 271 | 
             
                    add_target(component, change)
         | 
| 272 | 
            +
                  when "PBXAggregateTarget"
         | 
| 273 | 
            +
                    add_aggregate_target(component, change)
         | 
| 268 274 | 
             
                  when "PBXFileReference"
         | 
| 269 275 | 
             
                    add_file_reference(component, change)
         | 
| 270 276 | 
             
                  when "PBXGroup"
         | 
| @@ -298,21 +304,36 @@ module Kintsugi | |
| 298 304 | 
             
                  when "PBXReferenceProxy"
         | 
| 299 305 | 
             
                    add_reference_proxy(component, change)
         | 
| 300 306 | 
             
                  else
         | 
| 301 | 
            -
                    raise "Trying to add unsupported component type #{change["isa"]}. Full  | 
| 302 | 
            -
                      "is: #{change}"
         | 
| 307 | 
            +
                    raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
         | 
| 308 | 
            +
                      "component change is: #{change}"
         | 
| 303 309 | 
             
                  end
         | 
| 304 310 | 
             
                end
         | 
| 305 311 |  | 
| 306 312 | 
             
                def add_reference_proxy(containing_component, change)
         | 
| 307 313 | 
             
                  case containing_component
         | 
| 308 314 | 
             
                  when Xcodeproj::Project::PBXBuildFile
         | 
| 309 | 
            -
                     | 
| 315 | 
            +
                    # If there are two file references that refer to the same file, one with a build file and
         | 
| 316 | 
            +
                    # the other one without, this method will prefer to take the one without the build file.
         | 
| 317 | 
            +
                    # This assumes that it's preferred to have a file reference with build file than a file
         | 
| 318 | 
            +
                    # reference without/with two build files.
         | 
| 319 | 
            +
                    filter_references_without_build_files = lambda do |reference|
         | 
| 320 | 
            +
                      reference.referrers.find do |referrer|
         | 
| 321 | 
            +
                        referrer.is_a?(Xcodeproj::Project::PBXBuildFile)
         | 
| 322 | 
            +
                      end.nil?
         | 
| 323 | 
            +
                    end
         | 
| 324 | 
            +
                    file_reference =
         | 
| 325 | 
            +
                      find_reference_proxy(containing_component.project, change["remoteRef"],
         | 
| 326 | 
            +
                                           reference_filter: filter_references_without_build_files)
         | 
| 327 | 
            +
                    if file_reference.nil?
         | 
| 328 | 
            +
                      file_reference = find_reference_proxy(containing_component.project, change["remoteRef"])
         | 
| 329 | 
            +
                    end
         | 
| 330 | 
            +
                    containing_component.file_ref = file_reference
         | 
| 310 331 | 
             
                  when Xcodeproj::Project::PBXGroup
         | 
| 311 332 | 
             
                    reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
         | 
| 312 333 | 
             
                    containing_component << reference_proxy
         | 
| 313 334 | 
             
                    add_attributes_to_component(reference_proxy, change)
         | 
| 314 335 | 
             
                  else
         | 
| 315 | 
            -
                    raise "Trying to add reference proxy to an unsupported component type " \
         | 
| 336 | 
            +
                    raise MergeError, "Trying to add reference proxy to an unsupported component type " \
         | 
| 316 337 | 
             
                      "#{containing_component.isa}. Change is: #{change}"
         | 
| 317 338 | 
             
                  end
         | 
| 318 339 | 
             
                end
         | 
| @@ -327,7 +348,7 @@ module Kintsugi | |
| 327 348 | 
             
                    containing_component.children << variant_group
         | 
| 328 349 | 
             
                    add_attributes_to_component(variant_group, change)
         | 
| 329 350 | 
             
                  else
         | 
| 330 | 
            -
                    raise "Trying to add variant group to an unsupported component type " \
         | 
| 351 | 
            +
                    raise MergeError, "Trying to add variant group to an unsupported component type " \
         | 
| 331 352 | 
             
                      "#{containing_component.isa}. Change is: #{change}"
         | 
| 332 353 | 
             
                  end
         | 
| 333 354 | 
             
                end
         | 
| @@ -423,7 +444,7 @@ module Kintsugi | |
| 423 444 | 
             
                  when "PBXReferenceProxy"
         | 
| 424 445 | 
             
                    component.remote_ref = container_proxy
         | 
| 425 446 | 
             
                  else
         | 
| 426 | 
            -
                    raise "Trying to add container item proxy to an unsupported component type " \
         | 
| 447 | 
            +
                    raise MergeError, "Trying to add container item proxy to an unsupported component type " \
         | 
| 427 448 | 
             
                      "#{containing_component.isa}. Change is: #{change}"
         | 
| 428 449 | 
             
                  end
         | 
| 429 450 | 
             
                  add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
         | 
| @@ -458,7 +479,14 @@ module Kintsugi | |
| 458 479 | 
             
                end
         | 
| 459 480 |  | 
| 460 481 | 
             
                def add_subproject_reference(root_object, project_reference_change)
         | 
| 461 | 
            -
                   | 
| 482 | 
            +
                  filter_subproject_without_project_references = lambda do |file_reference|
         | 
| 483 | 
            +
                    root_object.project_references.find do |project_reference|
         | 
| 484 | 
            +
                      project_reference.project_ref.uuid == file_reference.uuid
         | 
| 485 | 
            +
                    end.nil?
         | 
| 486 | 
            +
                  end
         | 
| 487 | 
            +
                  subproject_reference =
         | 
| 488 | 
            +
                    find_file(root_object.project, project_reference_change["ProjectRef"],
         | 
| 489 | 
            +
                              file_filter: filter_subproject_without_project_references)
         | 
| 462 490 |  | 
| 463 491 | 
             
                  attribute =
         | 
| 464 492 | 
             
                    Xcodeproj::Project::PBXProject.references_by_keys_attributes
         | 
| @@ -488,6 +516,12 @@ module Kintsugi | |
| 488 516 | 
             
                  add_attributes_to_component(target, change)
         | 
| 489 517 | 
             
                end
         | 
| 490 518 |  | 
| 519 | 
            +
                def add_aggregate_target(root_object, change)
         | 
| 520 | 
            +
                  target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
         | 
| 521 | 
            +
                  root_object.project.targets << target
         | 
| 522 | 
            +
                  add_attributes_to_component(target, change)
         | 
| 523 | 
            +
                end
         | 
| 524 | 
            +
             | 
| 491 525 | 
             
                def add_file_reference(containing_component, change)
         | 
| 492 526 | 
             
                  # base configuration reference and product reference always reference a file that exists
         | 
| 493 527 | 
             
                  # inside a group, therefore in these cases the file is searched for.
         | 
| @@ -509,7 +543,7 @@ module Kintsugi | |
| 509 543 | 
             
                    file_reference.include_in_index = nil
         | 
| 510 544 | 
             
                    add_attributes_to_component(file_reference, change)
         | 
| 511 545 | 
             
                  else
         | 
| 512 | 
            -
                    raise "Trying to add file reference to an unsupported component type " \
         | 
| 546 | 
            +
                    raise MergeError, "Trying to add file reference to an unsupported component type " \
         | 
| 513 547 | 
             
                      "#{containing_component.isa}. Change is: #{change}"
         | 
| 514 548 | 
             
                  end
         | 
| 515 549 | 
             
                end
         | 
| @@ -524,8 +558,8 @@ module Kintsugi | |
| 524 558 | 
             
                    new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
         | 
| 525 559 | 
             
                    containing_component.children << new_group
         | 
| 526 560 | 
             
                  else
         | 
| 527 | 
            -
                    raise "Trying to add group to an unsupported component type  | 
| 528 | 
            -
                      "Change is: #{change}"
         | 
| 561 | 
            +
                    raise MergeError, "Trying to add group to an unsupported component type " \
         | 
| 562 | 
            +
                      "#{containing_component.isa}. Change is: #{change}"
         | 
| 529 563 | 
             
                  end
         | 
| 530 564 |  | 
| 531 565 | 
             
                  add_attributes_to_component(new_group, change)
         | 
| @@ -549,31 +583,35 @@ module Kintsugi | |
| 549 583 | 
             
                        add_child_to_component(component, added_attribute_element)
         | 
| 550 584 | 
             
                      end
         | 
| 551 585 | 
             
                    else
         | 
| 552 | 
            -
                      raise "Trying to add attribute of unsupported type '#{change_value.class}'  | 
| 553 | 
            -
                        "object #{component}. Attribute name is '#{change_name}'"
         | 
| 586 | 
            +
                      raise MergeError, "Trying to add attribute of unsupported type '#{change_value.class}' " \
         | 
| 587 | 
            +
                        "to object #{component}. Attribute name is '#{change_name}'"
         | 
| 554 588 | 
             
                    end
         | 
| 555 589 | 
             
                  end
         | 
| 556 590 | 
             
                end
         | 
| 557 591 |  | 
| 558 | 
            -
                def find_file(project, file_reference_change)
         | 
| 559 | 
            -
                   | 
| 560 | 
            -
             | 
| 561 | 
            -
             | 
| 562 | 
            -
             | 
| 563 | 
            -
                     | 
| 564 | 
            -
             | 
| 565 | 
            -
             | 
| 566 | 
            -
             | 
| 567 | 
            -
             | 
| 592 | 
            +
                def find_file(project, file_reference_change, file_filter: ->(_) { true })
         | 
| 593 | 
            +
                  file_references = project.files.select do |file_reference|
         | 
| 594 | 
            +
                    file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
         | 
| 595 | 
            +
                  end
         | 
| 596 | 
            +
                  if file_references.length > 1
         | 
| 597 | 
            +
                    puts "Debug: Found more than one matching file with path " \
         | 
| 598 | 
            +
                      "'#{file_reference_change["path"]}'. Using the first one."
         | 
| 599 | 
            +
                  elsif file_references.empty?
         | 
| 600 | 
            +
                    puts "Debug: No file reference found for file with path " \
         | 
| 601 | 
            +
                      "'#{file_reference_change["path"]}'."
         | 
| 602 | 
            +
                    return
         | 
| 568 603 | 
             
                  end
         | 
| 604 | 
            +
             | 
| 605 | 
            +
                  file_references.first
         | 
| 569 606 | 
             
                end
         | 
| 570 607 |  | 
| 571 | 
            -
                def find_reference_proxy(project, container_item_proxy_change)
         | 
| 608 | 
            +
                def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
         | 
| 572 609 | 
             
                  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
         | 
| 573 610 | 
             
                    project_ref_and_products[:product_group].children.find do |product|
         | 
| 574 611 | 
             
                      product.remote_ref.remote_global_id_string ==
         | 
| 575 612 | 
             
                        container_item_proxy_change["remoteGlobalIDString"] &&
         | 
| 576 | 
            -
                        product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
         | 
| 613 | 
            +
                        product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
         | 
| 614 | 
            +
                        reference_filter.call(product)
         | 
| 577 615 | 
             
                    end
         | 
| 578 616 | 
             
                  end.compact
         | 
| 579 617 |  | 
    
        data/lib/kintsugi/cli.rb
    ADDED
    
    | @@ -0,0 +1,203 @@ | |
| 1 | 
            +
            # Copyright (c) 2021 Lightricks. All rights reserved.
         | 
| 2 | 
            +
            # Created by Ben Yohay.
         | 
| 3 | 
            +
            # frozen_string_literal: true
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            require "optparse"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require_relative "version"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Kintsugi
         | 
| 10 | 
            +
              # Class resposible for creating the logic of various options for Kintsugi CLI.
         | 
| 11 | 
            +
              class CLI
         | 
| 12 | 
            +
                # Subcommands of Kintsugi CLI.
         | 
| 13 | 
            +
                attr_reader :subcommands
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                # Root command of Kintsugi CLI.
         | 
| 16 | 
            +
                attr_reader :root_command
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def initialize
         | 
| 19 | 
            +
                  @subcommands = {
         | 
| 20 | 
            +
                    "driver" => create_driver_subcommand,
         | 
| 21 | 
            +
                    "install-driver" => create_install_driver_subcommand,
         | 
| 22 | 
            +
                    "uninstall-driver" => create_uninstall_driver_subcommand
         | 
| 23 | 
            +
                  }.freeze
         | 
| 24 | 
            +
                  @root_command = create_root_command
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                private
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def create_driver_subcommand
         | 
| 32 | 
            +
                  option_parser =
         | 
| 33 | 
            +
                    OptionParser.new do |opts|
         | 
| 34 | 
            +
                      opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
         | 
| 35 | 
            +
                        "Uses Kintsugi as a Git merge driver. Parameters " \
         | 
| 36 | 
            +
                        "should be the path to base version of the file, path to ours version, path to " \
         | 
| 37 | 
            +
                        "theirs version, and the original file path."
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      opts.on("-h", "--help", "Prints this help") do
         | 
| 40 | 
            +
                        puts opts
         | 
| 41 | 
            +
                        exit
         | 
| 42 | 
            +
                      end
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  driver_action = lambda { |_, arguments|
         | 
| 46 | 
            +
                    if arguments.count != 4
         | 
| 47 | 
            +
                      puts "Incorrect number of arguments to 'driver' subcommand\n\n"
         | 
| 48 | 
            +
                      puts option_parser
         | 
| 49 | 
            +
                      exit(1)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                    Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
         | 
| 52 | 
            +
                  }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  Command.new(
         | 
| 55 | 
            +
                    option_parser: option_parser,
         | 
| 56 | 
            +
                    action: driver_action,
         | 
| 57 | 
            +
                    description: "3-way merge compatible with Git merge driver"
         | 
| 58 | 
            +
                  )
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def create_install_driver_subcommand
         | 
| 62 | 
            +
                  option_parser =
         | 
| 63 | 
            +
                    OptionParser.new do |opts|
         | 
| 64 | 
            +
                      opts.banner = "Usage: kintsugi install-driver\n" \
         | 
| 65 | 
            +
                        "Installs Kintsugi as a Git merge driver globally. "
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                      opts.on("-h", "--help", "Prints this help") do
         | 
| 68 | 
            +
                        puts opts
         | 
| 69 | 
            +
                        exit
         | 
| 70 | 
            +
                      end
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  action = lambda { |_, arguments|
         | 
| 74 | 
            +
                    if arguments.count != 0
         | 
| 75 | 
            +
                      puts "Incorrect number of arguments to 'install-driver' subcommand\n\n"
         | 
| 76 | 
            +
                      puts option_parser
         | 
| 77 | 
            +
                      exit(1)
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    if `which kintsugi`.chomp.empty?
         | 
| 81 | 
            +
                      puts "Can only install Kintsugi globally if Kintsugi is in your PATH"
         | 
| 82 | 
            +
                      exit(1)
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    install_kintsugi_driver_globally
         | 
| 86 | 
            +
                    puts "Done! 🪄"
         | 
| 87 | 
            +
                  }
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  Command.new(
         | 
| 90 | 
            +
                    option_parser: option_parser,
         | 
| 91 | 
            +
                    action: action,
         | 
| 92 | 
            +
                    description: "Installs Kintsugi as a Git merge driver globally"
         | 
| 93 | 
            +
                  )
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def install_kintsugi_driver_globally
         | 
| 97 | 
            +
                  `git config --global merge.kintsugi.name "Kintsugi driver"`
         | 
| 98 | 
            +
                  `git config --global merge.kintsugi.driver "kintsugi driver %O %A %B %P"`
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  attributes_file_path = global_attributes_file_path
         | 
| 101 | 
            +
                  merge_using_kintsugi_line = "'*.pbxproj merge=kintsugi'"
         | 
| 102 | 
            +
                  `grep -sqxF #{merge_using_kintsugi_line} "#{attributes_file_path}" \
         | 
| 103 | 
            +
                    || echo #{merge_using_kintsugi_line} >> "#{attributes_file_path}"`
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                def global_attributes_file_path
         | 
| 107 | 
            +
                  # The logic to decide the path to the global attributes file is described at:
         | 
| 108 | 
            +
                  # https://git-scm.com/docs/gitattributes.
         | 
| 109 | 
            +
                  config_attributes_file_path = `git config --global core.attributesfile`
         | 
| 110 | 
            +
                  return config_attributes_file_path unless config_attributes_file_path.empty?
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
         | 
| 113 | 
            +
                    File.join(ENV["HOME"], ".config/git/attributes")
         | 
| 114 | 
            +
                  else
         | 
| 115 | 
            +
                    File.join(ENV["XDG_CONFIG_HOME"], "git/attributes")
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def create_uninstall_driver_subcommand
         | 
| 120 | 
            +
                  option_parser =
         | 
| 121 | 
            +
                    OptionParser.new do |opts|
         | 
| 122 | 
            +
                      opts.banner = "Usage: kintsugi uninstall-driver\n" \
         | 
| 123 | 
            +
                        "Uninstalls Kintsugi as a Git merge driver that was previously installed globally."
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                      opts.on("-h", "--help", "Prints this help") do
         | 
| 126 | 
            +
                        puts opts
         | 
| 127 | 
            +
                        exit
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
                    end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  action = lambda { |_, arguments|
         | 
| 132 | 
            +
                    if arguments.count != 0
         | 
| 133 | 
            +
                      puts "Incorrect number of arguments to 'uninstall-driver' subcommand\n\n"
         | 
| 134 | 
            +
                      puts option_parser
         | 
| 135 | 
            +
                      exit(1)
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    uninstall_kintsugi_driver_globally
         | 
| 139 | 
            +
                    puts "Done!"
         | 
| 140 | 
            +
                  }
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                  Command.new(
         | 
| 143 | 
            +
                    option_parser: option_parser,
         | 
| 144 | 
            +
                    action: action,
         | 
| 145 | 
            +
                    description: "Uninstalls Kintsugi as a Git merge driver that was previously installed " \
         | 
| 146 | 
            +
                                 "globally."
         | 
| 147 | 
            +
                  )
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                def uninstall_kintsugi_driver_globally
         | 
| 151 | 
            +
                  `git config --global --unset merge.kintsugi.name`
         | 
| 152 | 
            +
                  `git config --global --unset merge.kintsugi.driver`
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  `sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{global_attributes_file_path}"`
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                def create_root_command
         | 
| 158 | 
            +
                  option_parser = OptionParser.new do |opts|
         | 
| 159 | 
            +
                    opts.banner = "Kintsugi, version #{Version::STRING}\n" \
         | 
| 160 | 
            +
                                  "Copyright (c) 2021 Lightricks\n\n" \
         | 
| 161 | 
            +
                                  "Usage: kintsugi <pbxproj_filepath> [options]\n" \
         | 
| 162 | 
            +
                                  "       kintsugi <subcommand> [options]"
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                    opts.separator ""
         | 
| 165 | 
            +
                    opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
         | 
| 166 | 
            +
                            "written in JSON format. Used for debug purposes.")
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                    opts.on("-h", "--help", "Prints this help") do
         | 
| 169 | 
            +
                      puts opts
         | 
| 170 | 
            +
                      exit
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    opts.on("-v", "--version", "Prints version") do
         | 
| 174 | 
            +
                      puts Version::STRING
         | 
| 175 | 
            +
                      exit
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    subcommands_descriptions = @subcommands.map do |command_name, command|
         | 
| 179 | 
            +
                      "    #{command_name}:    #{command.description}"
         | 
| 180 | 
            +
                    end.join("\n")
         | 
| 181 | 
            +
                    opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
         | 
| 182 | 
            +
                  end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                  root_action = lambda { |options, arguments|
         | 
| 185 | 
            +
                    if arguments.count != 1
         | 
| 186 | 
            +
                      puts "Incorrect number of arguments\n\n"
         | 
| 187 | 
            +
                      puts option_parser
         | 
| 188 | 
            +
                      exit(1)
         | 
| 189 | 
            +
                    end
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                    project_file_path = File.expand_path(arguments[0])
         | 
| 192 | 
            +
                    Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
         | 
| 193 | 
            +
                    puts "Resolved conflicts successfully"
         | 
| 194 | 
            +
                  }
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                  Command.new(
         | 
| 197 | 
            +
                    option_parser: option_parser,
         | 
| 198 | 
            +
                    action: root_action,
         | 
| 199 | 
            +
                    description: nil
         | 
| 200 | 
            +
                  )
         | 
| 201 | 
            +
                end
         | 
| 202 | 
            +
              end
         | 
| 203 | 
            +
            end
         | 
    
        data/lib/kintsugi/version.rb
    CHANGED
    
    
    
        data/lib/kintsugi.rb
    CHANGED
    
    | @@ -2,12 +2,14 @@ | |
| 2 2 | 
             
            # Created by Ben Yohay.
         | 
| 3 3 | 
             
            # frozen_string_literal: true
         | 
| 4 4 |  | 
| 5 | 
            +
            require "json"
         | 
| 5 6 | 
             
            require "tmpdir"
         | 
| 6 7 | 
             
            require "tempfile"
         | 
| 7 8 | 
             
            require "xcodeproj"
         | 
| 8 9 |  | 
| 9 10 | 
             
            require_relative "kintsugi/xcodeproj_extensions"
         | 
| 10 11 | 
             
            require_relative "kintsugi/apply_change_to_project"
         | 
| 12 | 
            +
            require_relative "kintsugi/error"
         | 
| 11 13 |  | 
| 12 14 | 
             
            module Kintsugi
         | 
| 13 15 | 
             
              class << self
         | 
| @@ -16,15 +18,15 @@ module Kintsugi | |
| 16 18 | 
             
                # @param  [String] project_file_path
         | 
| 17 19 | 
             
                #         Project to which to apply the changes.
         | 
| 18 20 | 
             
                #
         | 
| 19 | 
            -
                # @param  [String]  | 
| 21 | 
            +
                # @param  [String] changes_output_path
         | 
| 20 22 | 
             
                #         Path to where the changes to apply to the project are written in JSON format.
         | 
| 21 23 | 
             
                #
         | 
| 22 24 | 
             
                # @raise [ArgumentError]
         | 
| 23 | 
            -
                #        If the file extension is not `pbxproj | 
| 25 | 
            +
                #        If the file extension is not `pbxproj`, or the file doesn't exist, or if no rebase,
         | 
| 26 | 
            +
                #        cherry-pick, or merge is in progress
         | 
| 24 27 | 
             
                #
         | 
| 25 | 
            -
                # @raise [ | 
| 26 | 
            -
                #        If  | 
| 27 | 
            -
                #        opened, or there was an error applying the change to the project.
         | 
| 28 | 
            +
                # @raise [MergeError]
         | 
| 29 | 
            +
                #        If there was an error applying the change to the project.
         | 
| 28 30 | 
             
                #
         | 
| 29 31 | 
             
                # @return [void]
         | 
| 30 32 | 
             
                def resolve_conflicts(project_file_path, changes_output_path)
         | 
| @@ -39,43 +41,89 @@ module Kintsugi | |
| 39 41 | 
             
                    File.write(changes_output_path, JSON.pretty_generate(change))
         | 
| 40 42 | 
             
                  end
         | 
| 41 43 |  | 
| 42 | 
            -
                   | 
| 43 | 
            -
             | 
| 44 | 
            -
                  project_in_temp_directory.save
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  Dir.chdir(File.dirname(project_file_path)) do
         | 
| 47 | 
            -
                    `git reset #{project_file_path}`
         | 
| 48 | 
            -
                  end
         | 
| 49 | 
            -
                  FileUtils.cp(File.join(project_in_temp_directory.path, "project.pbxproj"), project_file_path)
         | 
| 44 | 
            +
                  apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
         | 
| 45 | 
            +
                end
         | 
| 50 46 |  | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 47 | 
            +
                # Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
         | 
| 48 | 
            +
                # `ours_project_path`. The files may not be at the original path, and therefore the
         | 
| 49 | 
            +
                # `original_project_path` is required in order for the project metadata to be written properly.
         | 
| 50 | 
            +
                #
         | 
| 51 | 
            +
                # @param  [String] base_project_path
         | 
| 52 | 
            +
                #         Path to the base version of the project.
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @param  [String] ours_project_path
         | 
| 55 | 
            +
                #         Path to ours version of the project.
         | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # @param  [String] theirs_project_path
         | 
| 58 | 
            +
                #         Path to theirs version of the project.
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                # @param  [String] original_project_path
         | 
| 61 | 
            +
                #         Path to the original path of the file.
         | 
| 62 | 
            +
                #
         | 
| 63 | 
            +
                # @raise [MergeError]
         | 
| 64 | 
            +
                #        If there was an error applying the change to the project.
         | 
| 65 | 
            +
                #
         | 
| 66 | 
            +
                # @return [void]
         | 
| 67 | 
            +
                def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
         | 
| 68 | 
            +
                                    original_project_path)
         | 
| 69 | 
            +
                  original_directory_name = File.basename(File.dirname(original_project_path))
         | 
| 70 | 
            +
                  base_temporary_project =
         | 
| 71 | 
            +
                    copy_project_to_temporary_path_in_directory_with_name(base_project_path,
         | 
| 72 | 
            +
                                                                          original_directory_name)
         | 
| 73 | 
            +
                  ours_temporary_project =
         | 
| 74 | 
            +
                    copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
         | 
| 75 | 
            +
                                                                          original_directory_name)
         | 
| 76 | 
            +
                  theirs_temporary_project =
         | 
| 77 | 
            +
                    copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
         | 
| 78 | 
            +
                                                                          original_directory_name)
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  change =
         | 
| 81 | 
            +
                    Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
         | 
| 82 | 
            +
                                                   :added, :removed)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
         | 
| 56 85 | 
             
                end
         | 
| 57 86 |  | 
| 58 87 | 
             
                private
         | 
| 59 88 |  | 
| 60 | 
            -
                 | 
| 61 | 
            -
                  if File.extname(project_file_path) != ".pbxproj"
         | 
| 62 | 
            -
                    raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
         | 
| 63 | 
            -
                  end
         | 
| 89 | 
            +
                PROJECT_FILE_NAME = "project.pbxproj"
         | 
| 64 90 |  | 
| 91 | 
            +
                def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
         | 
| 92 | 
            +
                  apply_change_to_project(project, change)
         | 
| 93 | 
            +
                  project.save
         | 
| 94 | 
            +
                  FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def validate_project(project_file_path)
         | 
| 65 98 | 
             
                  unless File.exist?(project_file_path)
         | 
| 66 99 | 
             
                    raise ArgumentError, "File '#{project_file_path}' doesn't exist"
         | 
| 67 100 | 
             
                  end
         | 
| 68 101 |  | 
| 102 | 
            +
                  if File.extname(project_file_path) != ".pbxproj"
         | 
| 103 | 
            +
                    raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
             | 
| 69 106 | 
             
                  Dir.chdir(File.dirname(project_file_path)) do
         | 
| 70 107 | 
             
                    unless file_has_base_ours_and_theirs_versions?(project_file_path)
         | 
| 71 | 
            -
                      raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts,  | 
| 72 | 
            -
                        "merge is not possible."
         | 
| 108 | 
            +
                      raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
         | 
| 109 | 
            +
                        "or a 3-way merge is not possible."
         | 
| 73 110 | 
             
                    end
         | 
| 74 111 | 
             
                  end
         | 
| 75 112 | 
             
                end
         | 
| 76 113 |  | 
| 114 | 
            +
                def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
         | 
| 115 | 
            +
                  temp_directory_name = File.join(Dir.mktmpdir, directory_name)
         | 
| 116 | 
            +
                  Dir.mkdir(temp_directory_name)
         | 
| 117 | 
            +
                  temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
         | 
| 118 | 
            +
                  FileUtils.cp(project_file_path, temp_project_file_path)
         | 
| 119 | 
            +
                  Xcodeproj::Project.open(File.dirname(temp_project_file_path))
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 77 122 | 
             
                def open_project_of_current_commit_in_temporary_directory(project_file_path)
         | 
| 78 | 
            -
                   | 
| 123 | 
            +
                  project_directory_name = File.basename(File.dirname(project_file_path))
         | 
| 124 | 
            +
                  temp_directory_name = File.join(Dir.mktmpdir, project_directory_name)
         | 
| 125 | 
            +
                  Dir.mkdir(temp_directory_name)
         | 
| 126 | 
            +
                  temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
         | 
| 79 127 | 
             
                  Dir.chdir(File.dirname(project_file_path)) do
         | 
| 80 128 | 
             
                    `git show HEAD:./project.pbxproj > #{temp_project_file_path}`
         | 
| 81 129 | 
             
                  end
         | 
| @@ -99,11 +147,11 @@ module Kintsugi | |
| 99 147 |  | 
| 100 148 | 
             
                def change_of_conflicting_commit_with_parent(project_file_path)
         | 
| 101 149 | 
             
                  Dir.chdir(File.dirname(project_file_path)) do
         | 
| 102 | 
            -
                    conflicting_commit_project_file_path = File.join(Dir.mktmpdir,  | 
| 103 | 
            -
                    `git show :3 | 
| 150 | 
            +
                    conflicting_commit_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
         | 
| 151 | 
            +
                    `git show :3:./#{PROJECT_FILE_NAME} > #{conflicting_commit_project_file_path}`
         | 
| 104 152 |  | 
| 105 | 
            -
                    conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir,  | 
| 106 | 
            -
                    `git show :1 | 
| 153 | 
            +
                    conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
         | 
| 154 | 
            +
                    `git show :1:./#{PROJECT_FILE_NAME} > #{conflicting_commit_parent_project_file_path}`
         | 
| 107 155 |  | 
| 108 156 | 
             
                    conflicting_commit_project = Xcodeproj::Project.open(
         | 
| 109 157 | 
             
                      File.dirname(conflicting_commit_project_file_path)
         | 
| @@ -38,6 +38,18 @@ describe Kintsugi, :apply_change_to_project do | |
| 38 38 | 
             
                expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 39 39 | 
             
              end
         | 
| 40 40 |  | 
| 41 | 
            +
              it "adds new aggregate target" do
         | 
| 42 | 
            +
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 43 | 
            +
                theirs_project.new_aggregate_target("foo")
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                described_class.apply_change_to_project(base_project, changes_to_apply)
         | 
| 48 | 
            +
                base_project.save
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 41 53 | 
             
              it "adds new subproject" do
         | 
| 42 54 | 
             
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 43 55 | 
             
                add_new_subproject_to_project(theirs_project, "foo", "foo")
         | 
| @@ -50,6 +62,26 @@ describe Kintsugi, :apply_change_to_project do | |
| 50 62 | 
             
                expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
         | 
| 51 63 | 
             
              end
         | 
| 52 64 |  | 
| 65 | 
            +
              it "adds subproject that already exists" do
         | 
| 66 | 
            +
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                subproject = add_new_subproject_to_project(theirs_project, "foo", "foo")
         | 
| 69 | 
            +
                theirs_project.save
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                ours_project = create_copy_of_project(base_project.path, "ours")
         | 
| 72 | 
            +
                add_existing_subproject_to_project(ours_project, subproject, "foo")
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                described_class.apply_change_to_project(ours_project, changes_to_apply)
         | 
| 77 | 
            +
                ours_project.save
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                expect(ours_project.root_object.project_references[0][:project_ref].uuid)
         | 
| 80 | 
            +
                  .not_to equal(ours_project.root_object.project_references[1][:project_ref].uuid)
         | 
| 81 | 
            +
                expect(ours_project.root_object.project_references[0][:project_ref].proxy_containers).not_to be_empty
         | 
| 82 | 
            +
                expect(ours_project.root_object.project_references[1][:project_ref].proxy_containers).not_to be_empty
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
             | 
| 53 85 | 
             
              # Checks that the order the changes are applied in is correct.
         | 
| 54 86 | 
             
              it "adds new subproject and reference to its framework" do
         | 
| 55 87 | 
             
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| @@ -144,6 +176,23 @@ describe Kintsugi, :apply_change_to_project do | |
| 144 176 | 
             
                  expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 145 177 | 
             
                end
         | 
| 146 178 |  | 
| 179 | 
            +
                it "changes simple attribute of a file that has a build file" do
         | 
| 180 | 
            +
                  target = base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
         | 
| 181 | 
            +
                  file_reference = base_project.main_group.find_file_by_path(filepath)
         | 
| 182 | 
            +
                  target.frameworks_build_phase.add_file_reference(file_reference)
         | 
| 183 | 
            +
                  base_project.save
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 186 | 
            +
                  file_reference = theirs_project.main_group.find_file_by_path(filepath)
         | 
| 187 | 
            +
                  file_reference.include_in_index = "4"
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                  described_class.apply_change_to_project(base_project, changes_to_apply)
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 194 | 
            +
                end
         | 
| 195 | 
            +
             | 
| 147 196 | 
             
                it "removes build files of a removed file" do
         | 
| 148 197 | 
             
                  target = base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
         | 
| 149 198 | 
             
                  target.source_build_phase.add_file_reference(
         | 
| @@ -417,6 +466,30 @@ describe Kintsugi, :apply_change_to_project do | |
| 417 466 | 
             
                  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
         | 
| 418 467 | 
             
                end
         | 
| 419 468 |  | 
| 469 | 
            +
                it "adds build file to a file reference that already exist" do
         | 
| 470 | 
            +
                  file_reference = base_project.main_group.new_reference("bar")
         | 
| 471 | 
            +
                  base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
         | 
| 472 | 
            +
             | 
| 473 | 
            +
                  base_project.main_group.new_reference("bar")
         | 
| 474 | 
            +
             | 
| 475 | 
            +
                  base_project.save
         | 
| 476 | 
            +
             | 
| 477 | 
            +
                  theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 478 | 
            +
             | 
| 479 | 
            +
                  theirs_file_reference = theirs_project.main_group.files.find do |file|
         | 
| 480 | 
            +
                    !file.referrers.find { |referrer| referrer.is_a?(Xcodeproj::Project::PBXBuildFile) } &&
         | 
| 481 | 
            +
                      file.display_name == "bar"
         | 
| 482 | 
            +
                  end
         | 
| 483 | 
            +
                  theirs_project.targets[0].frameworks_build_phase.add_file_reference(theirs_file_reference)
         | 
| 484 | 
            +
             | 
| 485 | 
            +
                  changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                  described_class.apply_change_to_project(base_project, changes_to_apply)
         | 
| 488 | 
            +
                  base_project.save
         | 
| 489 | 
            +
             | 
| 490 | 
            +
                  expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 491 | 
            +
                end
         | 
| 492 | 
            +
             | 
| 420 493 | 
             
                it "adds file reference to build file" do
         | 
| 421 494 | 
             
                  file_reference = base_project.main_group.new_reference("bar")
         | 
| 422 495 |  | 
| @@ -770,7 +843,44 @@ describe Kintsugi, :apply_change_to_project do | |
| 770 843 | 
             
                expect(base_project).to be_equivalent_to_project(theirs_project)
         | 
| 771 844 | 
             
              end
         | 
| 772 845 |  | 
| 773 | 
            -
              it " | 
| 846 | 
            +
              it "removes attribute target changes from a project it was removed from already" do
         | 
| 847 | 
            +
                base_project.root_object.attributes["TargetAttributes"] =
         | 
| 848 | 
            +
                  {"foo" => {"LastSwiftMigration" => "1140"}}
         | 
| 849 | 
            +
                base_project.save
         | 
| 850 | 
            +
             | 
| 851 | 
            +
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 852 | 
            +
                theirs_project.root_object.attributes["TargetAttributes"]["foo"] = {}
         | 
| 853 | 
            +
             | 
| 854 | 
            +
                ours_project = create_copy_of_project(base_project.path, "ours")
         | 
| 855 | 
            +
                ours_project.root_object.attributes["TargetAttributes"]["foo"] = {}
         | 
| 856 | 
            +
             | 
| 857 | 
            +
                changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 858 | 
            +
             | 
| 859 | 
            +
                described_class.apply_change_to_project(ours_project, changes_to_apply)
         | 
| 860 | 
            +
                ours_project.save
         | 
| 861 | 
            +
             | 
| 862 | 
            +
                expect(ours_project).to be_equivalent_to_project(theirs_project)
         | 
| 863 | 
            +
              end
         | 
| 864 | 
            +
             | 
| 865 | 
            +
              it "doesn't throw if existing attribute target change is same as added change" do
         | 
| 866 | 
            +
                base_project.root_object.attributes["TargetAttributes"] = {"foo" => "1140"}
         | 
| 867 | 
            +
                base_project.save
         | 
| 868 | 
            +
             | 
| 869 | 
            +
                theirs_project = create_copy_of_project(base_project.path, "theirs")
         | 
| 870 | 
            +
                theirs_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
         | 
| 871 | 
            +
             | 
| 872 | 
            +
                ours_project = create_copy_of_project(base_project.path, "ours")
         | 
| 873 | 
            +
                ours_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
         | 
| 874 | 
            +
             | 
| 875 | 
            +
                changes_to_apply = get_diff(theirs_project, base_project)
         | 
| 876 | 
            +
             | 
| 877 | 
            +
                described_class.apply_change_to_project(ours_project, changes_to_apply)
         | 
| 878 | 
            +
                ours_project.save
         | 
| 879 | 
            +
             | 
| 880 | 
            +
                expect(ours_project).to be_equivalent_to_project(theirs_project)
         | 
| 881 | 
            +
              end
         | 
| 882 | 
            +
             | 
| 883 | 
            +
              it "identifies subproject added at separate times when adding a product to the subproject" do
         | 
| 774 884 | 
             
                framework_filename = "baz"
         | 
| 775 885 |  | 
| 776 886 | 
             
                subproject = new_subproject("subproj", framework_filename)
         | 
| @@ -851,10 +961,10 @@ describe Kintsugi, :apply_change_to_project do | |
| 851 961 | 
             
                  file_reference.path == subproject_product_name
         | 
| 852 962 | 
             
                end.remove_from_project
         | 
| 853 963 |  | 
| 854 | 
            -
                project.root_object.project_references[ | 
| 964 | 
            +
                project.root_object.project_references[-1][:product_group] =
         | 
| 855 965 | 
             
                  project.new(Xcodeproj::Project::PBXGroup)
         | 
| 856 | 
            -
                project.root_object.project_references[ | 
| 857 | 
            -
                project.root_object.project_references[ | 
| 966 | 
            +
                project.root_object.project_references[-1][:product_group].name = "Products"
         | 
| 967 | 
            +
                project.root_object.project_references[-1][:product_group] <<
         | 
| 858 968 | 
             
                  create_reference_proxy_from_product_reference(project, subproject_reference,
         | 
| 859 969 | 
             
                                                                subproject.products_group.files[0])
         | 
| 860 970 | 
             
              end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,29 +1,35 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: kintsugi
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.4.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Ben Yohay
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2022-01-02 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: xcodeproj
         | 
| 15 15 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 16 | 
             
                requirements:
         | 
| 17 | 
            -
                - -  | 
| 17 | 
            +
                - - ">="
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 19 | 
             
                    version: 1.19.0
         | 
| 20 | 
            +
                - - "<="
         | 
| 21 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            +
                    version: 1.21.0
         | 
| 20 23 | 
             
              type: :runtime
         | 
| 21 24 | 
             
              prerelease: false
         | 
| 22 25 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 26 | 
             
                requirements:
         | 
| 24 | 
            -
                - -  | 
| 27 | 
            +
                - - ">="
         | 
| 25 28 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 29 | 
             
                    version: 1.19.0
         | 
| 30 | 
            +
                - - "<="
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: 1.21.0
         | 
| 27 33 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 34 | 
             
              name: rake
         | 
| 29 35 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -116,6 +122,8 @@ files: | |
| 116 122 | 
             
            - kintsugi.gemspec
         | 
| 117 123 | 
             
            - lib/kintsugi.rb
         | 
| 118 124 | 
             
            - lib/kintsugi/apply_change_to_project.rb
         | 
| 125 | 
            +
            - lib/kintsugi/cli.rb
         | 
| 126 | 
            +
            - lib/kintsugi/error.rb
         | 
| 119 127 | 
             
            - lib/kintsugi/utils.rb
         | 
| 120 128 | 
             
            - lib/kintsugi/version.rb
         | 
| 121 129 | 
             
            - lib/kintsugi/xcodeproj_extensions.rb
         | 
| @@ -142,7 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 142 150 | 
             
                - !ruby/object:Gem::Version
         | 
| 143 151 | 
             
                  version: '0'
         | 
| 144 152 | 
             
            requirements: []
         | 
| 145 | 
            -
             | 
| 153 | 
            +
            rubyforge_project:
         | 
| 154 | 
            +
            rubygems_version: 2.7.6.3
         | 
| 146 155 | 
             
            signing_key:
         | 
| 147 156 | 
             
            specification_version: 4
         | 
| 148 157 | 
             
            summary: pbxproj files git conflicts solver
         |