Skip to content

Commit

Permalink
Handle cases where package repo URLs might have different protocol (s… (
Browse files Browse the repository at this point in the history
#3)

* Handle cases where package repo URLs might have different protocol (ssh or https) and/or .git suffix

* Improved ignore_repos, prep for 0.1.3 release

* Factor out Xcode and Git modules

* added tests for git

---------

Co-authored-by: Harold <[email protected]>
  • Loading branch information
hbmartin and Harold committed Mar 20, 2024
1 parent 5e5c3f7 commit 01fe42e
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 136 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -506,3 +506,6 @@ Performance/UnfreezeString:
Performance/UriDefaultParser:
Description: "Use `URI::DEFAULT_PARSER` instead of `URI::Parser.new`."
Enabled: true

RSpec/AnyInstance:
Enabled: false
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
danger-spm_version_updates (0.1.2)
danger-spm_version_updates (0.1.3)
danger-plugin-api (~> 1.0)
semantic (~> 1.6)
xcodeproj (~> 1.24)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ spm_version_updates.report_above_maximum = true
# Whether to report pre-release versions, default false
spm_version_updates.report_pre_releases = true

# A list of repositories to ignore entirely, must exactly match the URL as configured in the Xcode project
# A list of repository URLs for packages to ignore entirely
spm_version_updates.ignore_repos = ["https://github.com/pointfreeco/swift-snapshot-testing"]
```

Expand Down
2 changes: 1 addition & 1 deletion lib/spm_version_updates/gem_version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SpmVersionUpdates
VERSION = "0.1.2"
VERSION = "0.1.3"
end
56 changes: 56 additions & 0 deletions lib/spm_version_updates/git.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Git
# Removes protocol and trailing .git from a repo URL
# @param [String] repo_url
# The URL of the repository
# @return [String]
def self.trim_repo_url(repo_url)
repo_url.split("://").last.gsub(/\.git$/, "")
end

# Extract a readable name for the repo given the url, generally org/repo
# @return [String]
def self.repo_name(repo_url)
match = repo_url.match(%r{([\w-]+/[\w-]+)(.git)?$})

if match
match[1] || match[0]
else
repo_url
end
end

# Call git to list tags
# @param [String] repo_url
# The URL of the dependency's repository
# @return [Array<Semantic::Version>]
def self.version_tags(repo_url)
versions = `git ls-remote -t #{repo_url}`
.split("\n")
.map { |line| line.split("/tags/").last }
.filter_map { |line|
begin
Semantic::Version.new(line)
rescue ArgumentError
nil
end
}
versions.sort!
versions.reverse!
versions
end

# Call git to find the last commit on a branch
# @param [String] repo_url
# The URL of the dependency's repository
# @param [String] branch_name
# The name of the branch on which to find the last commit
# @return [String]
def self.branch_last_commit(repo_url, branch_name)
`git ls-remote -h #{repo_url}`
.split("\n")
.find { |line| line.split("\trefs/heads/")[1] == branch_name }
.split("\trefs/heads/")[0]
end
end
127 changes: 12 additions & 115 deletions lib/spm_version_updates/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# frozen_string_literal: true

require "semantic"
require "xcodeproj"
require_relative "git"
require_relative "xcode"

module Danger
# A Danger plugin for checking if there are versions upgrades available for SPM dependencies
Expand All @@ -25,23 +26,26 @@ class DangerSpmVersionUpdates < Plugin
# @return [Boolean]
attr_accessor :report_pre_releases

# A list of repositories to ignore entirely, must exactly match the URL as configured in the Xcode project
# A list of repository URLs for packages to ignore entirely
# @return [Array<String>]
attr_accessor :ignore_repos

# A method that you can call from your Dangerfile
# @param [String] xcodeproj_path
# The path to your Xcode project
# @raise [XcodeprojPathMustBeSet] if the xcodeproj_path is blank
# @return [void]
def check_for_updates(xcodeproj_path)
remote_packages = get_remote_package(xcodeproj_path)
resolved_versions = get_resolved_versions(xcodeproj_path)
remote_packages = Xcode.get_packages(xcodeproj_path)
resolved_versions = Xcode.get_resolved_versions(xcodeproj_path)
$stderr.puts("Found resolved versions for #{resolved_versions.size} packages")

self.ignore_repos = self.ignore_repos&.map! { |repo| Git.trim_repo_url(repo) }

remote_packages.each { |repository_url, requirement|
next if ignore_repos&.include?(repository_url)
next if self.ignore_repos&.include?(repository_url)

name = repo_name(repository_url)
name = Git.repo_name(repository_url)
resolved_version = resolved_versions[repository_url]
kind = requirement["kind"]

Expand All @@ -52,12 +56,12 @@ def check_for_updates(xcodeproj_path)

if kind == "branch"
branch = requirement["branch"]
last_commit = git_branch_last_commit(repository_url, branch)
last_commit = Git.branch_last_commit(repository_url, branch)
warn("Newer commit available for #{name} (#{branch}): #{last_commit}") unless last_commit == resolved_version
next
end

available_versions = git_versions(repository_url)
available_versions = Git.version_tags(repository_url)
next if available_versions.first.to_s == resolved_version

if kind == "exactVersion" && @check_when_exact
Expand All @@ -72,74 +76,6 @@ def check_for_updates(xcodeproj_path)
}
end

# Extracts remote packages from an Xcode project
# @param [String] xcodeproj_path
# The path to your Xcode project
# @return [Hash<String, Hash>]
def get_remote_package(xcodeproj_path)
raise(XcodeprojPathMustBeSet) if xcodeproj_path.nil?

filter_remote_packages(Xcodeproj::Project.open(xcodeproj_path))
end

# Extracts resolved versions from Package.resolved relative to an Xcode project
# @param [String] xcodeproj_path
# The path to your Xcode project
# @return [Hash<String, String>]
def get_resolved_versions(xcodeproj_path)
resolved_paths = find_packages_resolved_file(xcodeproj_path)
raise(CouldNotFindResolvedFile) if resolved_paths.empty?

resolved_versions = resolved_paths.map { |resolved_path|
JSON.load_file!(resolved_path)["pins"]
.to_h { |pin|
[pin["location"], pin["state"]["version"] || pin["state"]["revision"]]
}
}
resolved_versions.reduce(:merge!)
end

# Extract a readable name for the repo given the url, generally org/repo
# @return [String]
def repo_name(repo_url)
match = repo_url.match(%r{([\w-]+/[\w-]+)(.git)?$})

if match
match[1] || match[0]
else
repo_url
end
end

# Find the configured SPM dependencies in the xcodeproj
# @return [Hash<String, Hash>]
def filter_remote_packages(project)
project.objects.select { |obj|
obj.kind_of?(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) &&
obj.requirement["kind"] != "commit"
}
.to_h { |package| [package.repositoryURL, package.requirement] }
end

# Find the Packages.resolved file
# @return [Array<String>]
def find_packages_resolved_file(xcodeproj_path)
locations = []
# First check the workspace for a resolved file
workspace = xcodeproj_path.sub("xcodeproj", "xcworkspace")
if Dir.exist?(workspace)
path = File.join(workspace, "xcshareddata", "swiftpm", "Package.resolved")
locations << path if File.exist?(path)
end

# Then check the project for a resolved file
path = File.join(xcodeproj_path, "project.xcworkspace", "xcshareddata", "swiftpm", "Package.resolved")
locations << path if File.exist?(path)

$stderr.puts("Searching for resolved packages in: #{locations}")
locations
end

private

def warn_for_new_versions_exact(available_versions, name, resolved_version)
Expand Down Expand Up @@ -188,44 +124,5 @@ def warn_for_new_versions(major_or_minor, available_versions, name, resolved_ver
TEXT
) unless newest_above_reqs == newest_meeting_reqs || newest_meeting_reqs.to_s == resolved_version
end

# Call git to list tags
# @param [String] repo_url
# The URL of the dependency's repository
# @return [Array<Semantic::Version>]
def git_versions(repo_url)
versions = `git ls-remote -t #{repo_url}`
.split("\n")
.map { |line| line.split("/tags/").last }
.filter_map { |line|
begin
Semantic::Version.new(line)
rescue ArgumentError
nil
end
}
versions.sort!
versions.reverse!
versions
end

# Calkl git to find the last commit on a branch
# @param [String] repo_url
# The URL of the dependency's repository
# @param [String] branch_name
# The name of the branch on which to find the last commit
# @return [String]
def git_branch_last_commit(repo_url, branch_name)
`git ls-remote -h #{repo_url}`
.split("\n")
.find { |line| line.split("\trefs/heads/")[1] == branch_name }
.split("\trefs/heads/")[0]
end
end

class XcodeprojPathMustBeSet < StandardError
end

class CouldNotFindResolvedFile < StandardError
end
end
67 changes: 67 additions & 0 deletions lib/spm_version_updates/xcode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require "xcodeproj"

module Xcode
# Find the configured SPM dependencies in the xcodeproj
# @param [String] xcodeproj_path
# The path of the Xcode project
# @return [Hash<String, Hash>]
def self.get_packages(xcodeproj_path)
raise(XcodeprojPathMustBeSet) if xcodeproj_path.nil? || xcodeproj_path.empty?

project = Xcodeproj::Project.open(xcodeproj_path)
project.objects.select { |obj|
obj.kind_of?(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) &&
obj.requirement["kind"] != "commit"
}
.to_h { |package|
[Git.trim_repo_url(package.repositoryURL), package.requirement]
}
end

# Extracts resolved versions from Package.resolved relative to an Xcode project
# @param [String] xcodeproj_path
# The path to your Xcode project
# @raise [CouldNotFindResolvedFile] if no Package.resolved files were found
# @return [Hash<String, String>]
def self.get_resolved_versions(xcodeproj_path)
resolved_paths = find_packages_resolved_file(xcodeproj_path)
raise(CouldNotFindResolvedFile) if resolved_paths.empty?

resolved_versions = resolved_paths.map { |resolved_path|
JSON.load_file!(resolved_path)["pins"]
.to_h { |pin|
[Git.trim_repo_url(pin["location"]), pin["state"]["version"] || pin["state"]["revision"]]
}
}
resolved_versions.reduce(:merge!)
end

# Find the Packages.resolved file
# @return [Array<String>]
def self.find_packages_resolved_file(xcodeproj_path)
locations = []
# First check the workspace for a resolved file
workspace = xcodeproj_path.sub("xcodeproj", "xcworkspace")
if Dir.exist?(workspace)
path = File.join(workspace, "xcshareddata", "swiftpm", "Package.resolved")
locations << path if File.exist?(path)
end

# Then check the project for a resolved file
path = File.join(xcodeproj_path, "project.xcworkspace", "xcshareddata", "swiftpm", "Package.resolved")
locations << path if File.exist?(path)

$stderr.puts("Searching for resolved packages in: #{locations}")
locations
end

private_class_method :find_packages_resolved_file

class XcodeprojPathMustBeSet < StandardError
end

class CouldNotFindResolvedFile < StandardError
end
end
Loading

0 comments on commit 01fe42e

Please sign in to comment.