Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring Resolver #659

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion spec/unit/fossil_resolver_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ private def resolver(name)
end

module Shards
# Allow overriding `source` for the specs
class FossilResolver
# Allow overriding `source` for the specs
def source=(@source)
@origin_url = nil # This needs to be cleared so that #origin_url re-runs `fossil remote-url`
end

# Direct set tool version for testing
def self.set_version(verstr : String)
@@version = verstr
end
end

describe FossilResolver, tags: %w[fossil] do
Expand Down Expand Up @@ -164,5 +169,39 @@ module Shards
resolver.matches_ref?(FossilCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+fossil.commit.1234567890abcdef")).should be_true
resolver.matches_ref?(FossilCommitRef.new("1234567"), Shards::Version.new("0.1.0.+fossil.commit.1234567890abcdef")).should be_true
end

it "#supports_format_arg" do
installed_version = FossilResolver.version.not_nil!
resolver = FossilResolver.new("", "")
FossilResolver.set_version "1.37"
resolver.supports_format_arg?.should eq false
FossilResolver.set_version "2.12"
resolver.supports_format_arg?.should eq false
FossilResolver.set_version "2.13"
resolver.supports_format_arg?.should eq false
FossilResolver.set_version "2.14"
resolver.supports_format_arg?.should eq true
FossilResolver.set_version "2.15"
resolver.supports_format_arg?.should eq true
FossilResolver.set_version "3.0"
resolver.supports_format_arg?.should eq true
FossilResolver.set_version installed_version
end

it "#supports_workdir_arg" do
installed_version = FossilResolver.version.not_nil!
resolver = FossilResolver.new("", "")
FossilResolver.set_version "1.37"
resolver.supports_workdir_arg?.should eq false
FossilResolver.set_version "2.11"
resolver.supports_workdir_arg?.should eq false
FossilResolver.set_version "2.12"
resolver.supports_workdir_arg?.should eq true
FossilResolver.set_version "2.13"
resolver.supports_workdir_arg?.should eq true
FossilResolver.set_version "3.0"
resolver.supports_workdir_arg?.should eq true
FossilResolver.set_version installed_version
end
end
end
221 changes: 38 additions & 183 deletions src/resolvers/fossil.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require "uri"
require "./resolver"
require "./version_control"
require "../versions"
require "../logger"
require "../helpers"
Expand Down Expand Up @@ -89,14 +89,8 @@ module Shards
end
end

class FossilResolver < Resolver
@@has_fossil_command : Bool?
@@fossil_version_maj : Int8?
@@fossil_version_min : Int8?
@@fossil_version_rev : Int8?
@@fossil_version : String?

@origin_url : String?
class FossilResolver < VersionControlResolver
@@extension = ".fossil"
@local_fossil_file : String?

def self.key
Expand All @@ -112,38 +106,15 @@ module Shards
end
end

protected def self.has_fossil_command?
if @@has_fossil_command.nil?
@@has_fossil_command = (Process.run("fossil version", shell: true).success? rescue false)
protected def self.command?
if @@command.nil?
@@command = (Process.run("fossil version", shell: true).success? rescue false)
end
@@has_fossil_command
end

protected def self.fossil_version
unless @@fossil_version
@@fossil_version = `fossil version`[/version\s+([^\s]*)/, 1]
pieces = @@fossil_version.not_nil!.split('.')
@@fossil_version_maj = pieces[0].to_i8
@@fossil_version_min = pieces[1].to_i8
@@fossil_version_rev = (pieces[2]?.try &.to_i8 || 0i8)
end

@@fossil_version
end

protected def self.fossil_version_maj
self.fossil_version unless @@fossil_version_maj
@@fossil_version_maj.not_nil!
end

protected def self.fossil_version_min
self.fossil_version unless @@fossil_version_min
@@fossil_version_min.not_nil!
@@command
end

protected def self.fossil_version_rev
self.fossil_version unless @@fossil_version_rev
@@fossil_version_rev.not_nil!
protected def self.version
@@version ||= `fossil version`[/version\s+([^\s]*)/, 1]
end

def read_spec(version : Version) : String?
Expand Down Expand Up @@ -173,16 +144,6 @@ module Shards
end
end

private def spec?(version)
spec(version)
rescue Error
end

def available_releases : Array(Version)
update_local_cache
versions_from_tags
end

def latest_version_for_ref(ref : FossilRef?) : Version
update_local_cache
ref ||= FossilTrunkRef.new
Expand Down Expand Up @@ -229,20 +190,29 @@ module Shards

# The --workdir argument was introduced in version 2.12, so we have to
# fake it
if FossilResolver.fossil_version_maj <= 2 &&
FossilResolver.fossil_version_min < 12
if supports_workdir_arg?
run "fossil open #{local_fossil_file} #{Process.quote(ref.to_fossil_ref)} --nested --workdir #{install_path}"
else
Log.debug { "Opening Fossil repo #{local_fossil_file} in directory #{install_path}" }
run("fossil open #{local_fossil_file} #{Process.quote(ref.to_fossil_ref)} --nested", install_path)
else
run "fossil open #{local_fossil_file} #{Process.quote(ref.to_fossil_ref)} --nested --workdir #{install_path}"
end
end

def supports_workdir_arg? : Bool
Versions.compare("2.12", FossilResolver.version.not_nil!) >= 0
end

def supports_format_arg? : Bool
Versions.compare("2.14", FossilResolver.version.not_nil!) >= 0
end

def commit_sha1_at(ref : FossilRef)
# Fossil versions before 2.14 do not support the --format/-F for the
# timeline command.
if FossilResolver.fossil_version_maj <= 2 &&
FossilResolver.fossil_version_min < 14
if supports_format_arg?
# Fossil v2.14 and newer support -F %H, so use that.
capture("fossil timeline #{Process.quote(ref.to_fossil_ref)} -t ci -F %H -n 1 -R #{Process.quote(local_fossil_file)}").split('\n')[0]
else
# Capture the short artifact name from the timeline using a regex.
# -W 0 = unlimited line width
# -n 1 = limit results to one entry
Expand All @@ -262,15 +232,12 @@ module Shards
# name to the full artifact hash.
whatis = capture("fossil whatis #{retLines[0]} -R #{Process.quote(local_fossil_file)}")
/artifact:\s+(.+)/.match(whatis).try &.[1] || ""
else
# Fossil v2.14 and newer support -F %H, so use that.
capture("fossil timeline #{Process.quote(ref.to_fossil_ref)} -t ci -F %H -n 1 -R #{Process.quote(local_fossil_file)}").split('\n')[0]
end
end

def local_path
@local_path ||= begin
uri = parse_uri(fossil_url)
uri = parse_uri(vcs_url)

path = uri.path
path = Path[path]
Expand All @@ -291,10 +258,6 @@ module Shards
@local_fossil_file ||= Path[local_path].join("#{name}.fossil").normalize.to_s
end

def fossil_url
source.strip
end

def parse_requirement(params : Hash(String, String)) : Requirement
params.each do |key, value|
case key
Expand All @@ -313,7 +276,7 @@ module Shards

record FossilVersion, value : String, commit : String? = nil

private def parse_fossil_version(version : Version) : FossilVersion
private def parse_version(version : Version) : FossilVersion
case version.value
when VERSION_REFERENCE
FossilVersion.new version.value
Expand All @@ -325,37 +288,12 @@ module Shards
end

private def fossil_ref(version : Version) : FossilRef
fossil_version = parse_fossil_version(version)
if commit = fossil_version.commit
version = parse_version(version)
if commit = version.commit
FossilCommitRef.new commit
else
FossilTagRef.new "v#{fossil_version.value}"
end
end

def update_local_cache
if cloned_repository? && origin_changed?
delete_repository
@updated_cache = false
end

return if Shards.local? || @updated_cache
Log.info { "Fetching #{fossil_url}" }

if cloned_repository?
# repositories cloned with shards v0.8.0 won't fetch any new remote
# refs; we must delete them and clone again!
if valid_repository?
fetch_repository
else
delete_repository
mirror_repository
end
else
mirror_repository
FossilTagRef.new "v#{version.value}"
end

@updated_cache = true
end

private def mirror_repository
Expand All @@ -364,35 +302,22 @@ module Shards
Dir.mkdir_p(path)
FileUtils.rm(fossil_file) if File.exists?(fossil_file)

source = fossil_url
source = vcs_url
# Remove a "file://" from the beginning, otherwise the path might be invalid
# on Windows.
source = source.lchop("file://")

fossil_retry(err: "Failed to clone #{source}") do
run_in_current_folder "fossil clone #{Process.quote(source)} #{Process.quote(fossil_file)}"
vcs_retry(err: "Failed to clone #{source}") do
run_in_folder "fossil clone #{Process.quote(source)} #{Process.quote(fossil_file)}"
end
end

private def fetch_repository
fossil_retry(err: "Failed to update #{fossil_url}") do
vcs_retry(err: "Failed to update #{vcs_url}") do
run "fossil pull -R #{Process.quote(local_fossil_file)}"
end
end

private def fossil_retry(err = "Failed to fetch repository", &)
retries = 0
loop do
yield
break
rescue inner_err : Error
retries += 1
next if retries < 3
Log.debug { inner_err }
raise Error.new("#{err}: #{inner_err}")
end
end

private def delete_repository
Log.debug { "rm -rf #{Process.quote(local_path)}'" }
Shards::Helpers.rm_rf(local_path)
Expand All @@ -412,93 +337,23 @@ module Shards
File.exists?(local_fossil_file)
end

protected def origin_url
private def origin_url
@origin_url ||= capture("fossil remote-url -R #{Process.quote(local_fossil_file)}").strip
end

# Returns whether origin URLs have differing hosts and/or paths.
protected def origin_changed?
return false if origin_url == fossil_url
return true if origin_url.nil? || fossil_url.nil?

origin_parsed = parse_uri(origin_url)
fossil_parsed = parse_uri(fossil_url)

(origin_parsed.host != fossil_parsed.host) || (origin_parsed.path != fossil_parsed.path)
end

# Parses a URI string
private def parse_uri(raw_uri)
# Need to check for file URIs early, otherwise generic parsing will fail on a colon.
if (path = raw_uri.lchop?("file://"))
return URI.new(scheme: "file", path: path)
end

# Try normal URI parsing first
uri = URI.parse(raw_uri)
return uri if uri.absolute? && !uri.opaque?

# Otherwise, assume and attempt to parse the scp-style ssh URIs
host, _, path = raw_uri.partition(':')

if host.includes?('@')
user, _, host = host.partition('@')
end

# Normalize leading slash, matching URI parsing
unless path.starts_with?('/')
path = '/' + path
end

URI.new(scheme: "ssh", host: host, path: path, user: user)
end

private def file_exists?(ref : FossilRef, path)
files = capture("fossil ls -R #{Process.quote(local_fossil_file)} -r #{Process.quote(ref.to_fossil_ref)} #{Process.quote(path)}")
!files.strip.empty?
end

private def capture(command, path = local_path)
run(command, capture: true, path: path).not_nil!
end

private def run(command, path = local_path, capture = false)
if Shards.local? && !Dir.exists?(path)
dependency_name = File.basename(path, ".fossil")
raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.")
end
Dir.cd(path) do
run_in_current_folder(command, capture)
end
end

private def run_in_current_folder(command, capture = false)
unless FossilResolver.has_fossil_command?
private def error_if_command_is_missing
unless FossilResolver.command?
raise Error.new("Error missing fossil command line tool. Please install Fossil first!")
end

Log.debug { command }

STDERR.flush
output = capture ? IO::Memory.new : Process::Redirect::Close
error = IO::Memory.new
status = Process.run(command, shell: true, output: output, error: error)

if status.success?
output.to_s if capture
else
message = error.to_s
raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?")
end
end

def report_version(version : Version) : String
fossil_version = parse_fossil_version(version)
if commit = fossil_version.commit
"#{fossil_version.value} at #{commit[0...7]}"
else
version.value
end
private def error_for_run_failure(command, message : String)
raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?")
end

register_resolver "fossil", FossilResolver
Expand Down
Loading