diff --git a/swift/lib/dependabot/swift/file_updater.rb b/swift/lib/dependabot/swift/file_updater.rb index 2055d2125a9a..6bdcdaea858f 100644 --- a/swift/lib/dependabot/swift/file_updater.rb +++ b/swift/lib/dependabot/swift/file_updater.rb @@ -2,16 +2,73 @@ require "dependabot/file_updaters" require "dependabot/file_updaters/base" +require "dependabot/swift/file_updater/lockfile_updater" +require "dependabot/swift/file_updater/manifest_updater" module Dependabot module Swift class FileUpdater < Dependabot::FileUpdaters::Base def self.updated_files_regex - raise NotImplementedError + [ + /Package(@swift-\d(\.\d){0,2})?\.swift/, + /^Package\.resolved$/ + ] end def updated_dependency_files - raise NotImplementedError + updated_files = [] + + SharedHelpers.in_a_temporary_repo_directory(manifest.directory, repo_contents_path) do + updated_manifest = nil + + if file_changed?(manifest) + updated_manifest = updated_file(file: manifest, content: updated_manifest_content) + updated_files << updated_manifest + end + + updated_files << updated_file(file: lockfile, content: updated_lockfile_content(updated_manifest)) if lockfile + end + + updated_files + end + + private + + def dependency + # For now we will be updating a single dependency. + # TODO: Revisit when/if implementing full unlocks + dependencies.first + end + + def check_required_files + raise "A Package.swift file must be provided!" unless manifest + end + + def updated_manifest_content + ManifestUpdater.new( + manifest.content, + old_requirements: dependency.previous_requirements, + new_requirements: dependency.requirements + ).updated_manifest_content + end + + def updated_lockfile_content(updated_manifest) + LockfileUpdater.new( + dependencies: dependencies, + manifest: updated_manifest || manifest, + repo_contents_path: repo_contents_path, + credentials: credentials + ).updated_lockfile_content + end + + def manifest + @manifest ||= get_original_file("Package.swift") + end + + def lockfile + return @lockfile if defined?(@lockfile) + + @lockfile = get_original_file("Package.resolved") end end end diff --git a/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb b/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb new file mode 100644 index 000000000000..b0f45f9c7c20 --- /dev/null +++ b/swift/lib/dependabot/swift/file_updater/lockfile_updater.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "dependabot/file_updaters/base" +require "dependabot/shared_helpers" + +module Dependabot + module Swift + class FileUpdater < Dependabot::FileUpdaters::Base + class LockfileUpdater + def initialize(dependencies:, manifest:, repo_contents_path:, credentials:) + @dependencies = dependencies + @manifest = manifest + @repo_contents_path = repo_contents_path + @credentials = credentials + end + + def updated_lockfile_content + SharedHelpers.in_a_temporary_repo_directory(manifest.directory, repo_contents_path) do + File.write(manifest.name, manifest.content) + + SharedHelpers.with_git_configured(credentials: credentials) do + SharedHelpers.run_shell_command( + "swift package update #{dependencies.map(&:name).join(' ')}", + fingerprint: "swift package update " + ) + + File.read("Package.resolved") + end + end + end + + private + + attr_reader :dependencies, :manifest, :repo_contents_path, :credentials + end + end + end +end diff --git a/swift/lib/dependabot/swift/file_updater/manifest_updater.rb b/swift/lib/dependabot/swift/file_updater/manifest_updater.rb new file mode 100644 index 000000000000..5137550f5d15 --- /dev/null +++ b/swift/lib/dependabot/swift/file_updater/manifest_updater.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "dependabot/file_updaters/base" +require "dependabot/swift/file_updater/requirement_replacer" + +module Dependabot + module Swift + class FileUpdater < FileUpdaters::Base + class ManifestUpdater + def initialize(content, old_requirements:, new_requirements:) + @content = content + @old_requirements = old_requirements + @new_requirements = new_requirements + end + + def updated_manifest_content + updated_content = content + + old_requirements.zip(new_requirements).each do |old, new| + updated_content = RequirementReplacer.new( + content: updated_content, + declaration: old[:metadata][:declaration_string], + old_requirement: old[:metadata][:requirement_string], + new_requirement: new[:metadata][:requirement_string] + ).updated_content + end + + updated_content + end + + private + + attr_reader :content, :old_requirements, :new_requirements + end + end + end +end diff --git a/swift/lib/dependabot/swift/file_updater/requirement_replacer.rb b/swift/lib/dependabot/swift/file_updater/requirement_replacer.rb new file mode 100644 index 000000000000..6f1311cfdeda --- /dev/null +++ b/swift/lib/dependabot/swift/file_updater/requirement_replacer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "dependabot/file_updaters/base" + +module Dependabot + module Swift + class FileUpdater < Dependabot::FileUpdaters::Base + class RequirementReplacer + def initialize(content:, declaration:, old_requirement:, new_requirement:) + @content = content + @declaration = declaration + @old_requirement = old_requirement + @new_requirement = new_requirement + end + + def updated_content + content.gsub(declaration) do |match| + match.to_s.sub(old_requirement, new_requirement) + end + end + + private + + attr_reader :content, :declaration, :old_requirement, :new_requirement + end + end + end +end diff --git a/swift/spec/dependabot/swift/file_updater_spec.rb b/swift/spec/dependabot/swift/file_updater_spec.rb new file mode 100644 index 000000000000..5c55b6d9b2f9 --- /dev/null +++ b/swift/spec/dependabot/swift/file_updater_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/swift/file_updater" +require_common_spec "file_updaters/shared_examples_for_file_updaters" + +RSpec.describe Dependabot::Swift::FileUpdater do + it_behaves_like "a dependency file updater" + + subject(:updater) do + described_class.new( + dependency_files: files, + dependencies: dependencies, + credentials: credentials, + repo_contents_path: repo_contents_path + ) + end + + let(:project_name) { "Example" } + let(:repo_contents_path) { build_tmp_repo(project_name) } + + let(:files) { project_dependency_files(project_name) } + let(:dependencies) { [] } + let(:credentials) do + [{ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" }] + end + + describe "#updated_dependency_files" do + subject { updater.updated_dependency_files } + + let(:dependencies) do + [ + Dependabot::Dependency.new( + name: "reactiveswift", + version: "7.1.1", + previous_version: "7.1.0", + requirements: [{ + requirement: "= 7.1.1", + groups: [], + file: "Package.swift", + source: { + type: "git", + url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", + ref: "7.1.0", + branch: nil + }, + metadata: { + requirement_string: "exact: \"7.1.1\"" + } + }], + previous_requirements: [{ + requirement: "= 7.1.0", + groups: [], + file: "Package.swift", + source: { + type: "git", + url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", + ref: "7.1.0", + branch: nil + }, + metadata: { + declaration_string: + ".package(url: \"https://github.com/ReactiveCocoa/ReactiveSwift.git\",\n exact: \"7.1.0\")", + requirement_string: "exact: \"7.1.0\"" + } + }], + package_manager: "swift" + ) + ] + end + + it "updates the version in manifest and lockfile" do + manifest = subject.find { |file| file.name == "Package.swift" } + + expect(manifest.content).to include( + ".package(url: \"https://github.com/ReactiveCocoa/ReactiveSwift.git\",\n exact: \"7.1.1\")" + ) + + lockfile = subject.find { |file| file.name == "Package.resolved" } + + expect(lockfile.content.gsub(/^ {4}/, "")).to include <<~RESOLVED + { + "identity" : "reactiveswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", + "state" : { + "revision" : "40c465af19b993344e84355c00669ba2022ca3cd", + "version" : "7.1.1" + } + }, + RESOLVED + end + end +end