diff --git a/spec/unit/fossil_resolver_spec.cr b/spec/unit/fossil_resolver_spec.cr index 2095b4a2..1d9863d7 100644 --- a/spec/unit/fossil_resolver_spec.cr +++ b/spec/unit/fossil_resolver_spec.cr @@ -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 @@ -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 diff --git a/src/resolvers/fossil.cr b/src/resolvers/fossil.cr index b568994c..6c98936f 100644 --- a/src/resolvers/fossil.cr +++ b/src/resolvers/fossil.cr @@ -1,5 +1,5 @@ require "uri" -require "./resolver" +require "./version_control" require "../versions" require "../logger" require "../helpers" @@ -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 @@ -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? @@ -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 @@ -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 @@ -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] @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/src/resolvers/git.cr b/src/resolvers/git.cr index 955c8d4f..ce556031 100644 --- a/src/resolvers/git.cr +++ b/src/resolvers/git.cr @@ -1,5 +1,5 @@ require "uri" -require "./resolver" +require "./version_control" require "../versions" require "../logger" require "../helpers" @@ -89,12 +89,9 @@ module Shards end end - class GitResolver < Resolver - @@has_git_command : Bool? + class GitResolver < VersionControlResolver + @@extension = ".git" @@git_column_never : String? - @@git_version : String? - - @origin_url : String? def self.key "git" @@ -136,19 +133,19 @@ module Shards end end - protected def self.has_git_command? - if @@has_git_command.nil? - @@has_git_command = (Process.run("git", ["--version"]).success? rescue false) + protected def self.command? + if @@command.nil? + @@command = (Process.run("git", ["--version"]).success? rescue false) end - @@has_git_command + @@command end - protected def self.git_version - @@git_version ||= `git --version`.strip[12..-1] + protected def self.version + @@version ||= `git --version`.strip[12..-1] end protected def self.git_column_never - @@git_column_never ||= Versions.compare(git_version, "1.7.11") < 0 ? "--column=never" : "" + @@git_column_never ||= Versions.compare(version, "1.7.11") < 0 ? "--column=never" : "" end def read_spec(version : Version) : String? @@ -178,16 +175,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 : GitRef?) : Version update_local_cache ref ||= GitHeadRef.new @@ -234,7 +221,7 @@ module Shards def local_path @local_path ||= begin - uri = parse_uri(git_url) + uri = parse_uri(vcs_url) path = uri.path path += ".git" unless path.ends_with?(".git") @@ -252,10 +239,6 @@ module Shards end end - def git_url - source.strip - end - def parse_requirement(params : Hash(String, String)) : Requirement params.each do |key, value| case key @@ -274,7 +257,7 @@ module Shards record GitVersion, value : String, commit : String? = nil - private def parse_git_version(version : Version) : GitVersion + private def parse_version(version : Version) : GitVersion case version.value when VERSION_REFERENCE GitVersion.new version.value @@ -286,37 +269,12 @@ module Shards end private def git_ref(version : Version) : GitRef - git_version = parse_git_version(version) - if commit = git_version.commit + version = parse_version(version) + if commit = version.commit GitCommitRef.new commit else - GitTagRef.new "v#{git_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 #{git_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 + GitTagRef.new "v#{version.value}" end - - @updated_cache = true end private def mirror_repository @@ -327,32 +285,19 @@ module Shards # be used interactively. # This configuration can be overridden by defining the environment # variable `GIT_ASKPASS`. - git_retry(err: "Failed to clone #{git_url}") do - run_in_folder "git clone -c core.askPass=true -c init.templateDir= --mirror --quiet -- #{Process.quote(git_url)} #{Process.quote(local_path)}" + vcs_retry(err: "Failed to clone #{vcs_url}") do + run_in_folder "git clone -c core.askPass=true -c init.templateDir= --mirror --quiet -- #{Process.quote(vcs_url)} #{Process.quote(local_path)}" end end private def fetch_repository - git_retry(err: "Failed to update #{git_url}") do + vcs_retry(err: "Failed to update #{vcs_url}") do run "git fetch --all --quiet" end end - private def git_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)}'" } + Log.debug { "rm -rf #{Process.quote(local_path)}" } Shards::Helpers.rm_rf(local_path) @origin_url = nil end @@ -376,92 +321,23 @@ module Shards @origin_url ||= capture("git ls-remote --get-url origin").strip end - # Returns whether origin URLs have differing hosts and/or paths. - protected def origin_changed? - return false if origin_url == git_url - return true if origin_url.nil? || git_url.nil? - - origin_parsed = parse_uri(origin_url) - git_parsed = parse_uri(git_url) - - (origin_parsed.host != git_parsed.host) || (origin_parsed.path != git_parsed.path) - end - - # Parses a URI string, with additional support for ssh+git URI schemes. - 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 : GitRef, path) files = capture("git ls-tree -r --full-tree --name-only #{Process.quote(ref.to_git_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, ".git") - raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.") - end - run_in_folder(command, path, capture) - end - - # Chdir to a folder and run command. - # Runs in current folder if `path` is nil. - private def run_in_folder(command, path : String? = nil, capture = false) - unless GitResolver.has_git_command? + private def error_if_command_is_missing + unless GitResolver.command? raise Error.new("Error missing git command line tool. Please install Git first!") end - - Log.debug { command } - - output = capture ? IO::Memory.new : Process::Redirect::Close - error = IO::Memory.new - status = Process.run(command, shell: true, output: output, error: error, chdir: path) - - if status.success? - output.to_s if capture - else - str = error.to_s - if str.starts_with?("error: ") && (idx = str.index('\n')) - message = str[7...idx] - raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?") - else - raise Error.new("Failed #{command}.\n#{str}") - end - end end - def report_version(version : Version) : String - git_version = parse_git_version(version) - if commit = git_version.commit - "#{git_version.value} at #{commit[0...7]}" + private def error_for_run_failure(command, str : String) + if str.starts_with?("error: ") && (idx = str.index('\n')) + message = str[7...idx] + raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?") else - version.value + raise Error.new("Failed #{command}.\n#{str}") end end diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 7568b09d..c0e1a91a 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -1,5 +1,5 @@ require "uri" -require "./resolver" +require "./version_control" require "../versions" require "../logger" require "../helpers" @@ -127,11 +127,8 @@ module Shards end end - class HgResolver < Resolver - @@has_hg_command : Bool? - @@hg_version : String? - - @origin_url : String? + class HgResolver < VersionControlResolver + @@extension = "" def self.key "hg" @@ -146,15 +143,15 @@ module Shards end end - protected def self.has_hg_command? - if @@has_hg_command.nil? - @@has_hg_command = (Process.run("hg", ["--version"]).success? rescue false) + protected def self.command? + if @@command.nil? + @@command = (Process.run("hg", ["--version"]).success? rescue false) end - @@has_hg_command + @@command end - protected def self.hg_version - @@hg_version ||= `hg --version`[/\(version\s+([^)]*)\)/, 1] + protected def self.version + @@version ||= `hg --version`[/\(version\s+([^)]*)\)/, 1] end def read_spec(version : Version) : String? @@ -181,16 +178,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 : HgRef?) : Version update_local_cache ref ||= HgCurrentRef.new @@ -242,7 +229,7 @@ module Shards def local_path @local_path ||= begin - uri = parse_uri(hg_url) + uri = parse_uri(vcs_url) path = uri.path path = Path[path] @@ -259,10 +246,6 @@ module Shards end end - def hg_url - source.strip - end - def parse_requirement(params : Hash(String, String)) : Requirement params.each do |key, value| case key @@ -282,7 +265,7 @@ module Shards record HgVersion, value : String, commit : String? = nil - private def parse_hg_version(version : Version) : HgVersion + private def parse_version(version : Version) : HgVersion case version.value when VERSION_REFERENCE HgVersion.new version.value @@ -294,75 +277,39 @@ module Shards end private def hg_ref(version : Version) : HgRef - hg_version = parse_hg_version(version) - if commit = hg_version.commit + version = parse_version(version) + if commit = version.commit HgCommitRef.new commit else - HgTagRef.new "v#{hg_version.value}" + HgTagRef.new "v#{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 #{hg_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 - end - - @updated_cache = true - end - private def mirror_repository path = local_path FileUtils.rm_r(path) if File.exists?(path) Dir.mkdir_p(path) - source = hg_url + source = vcs_url # Remove a "file://" from the beginning, otherwise the path might be invalid # on Windows. source = source.lchop("file://") - hg_retry(err: "Failed to clone #{source}") do + vcs_retry(err: "Failed to clone #{source}") do # We checkout the working directory so that "." is meaningful. # # An alternative would be to use the `@` bookmark, but only as long # as nothing new is committed. - run_in_current_folder "hg clone --quiet -- #{Process.quote(source)} #{Process.quote(path)}" + run_in_folder "hg clone --quiet -- #{Process.quote(source)} #{Process.quote(path)}" end end private def fetch_repository - hg_retry(err: "Failed to update #{hg_url}") do + vcs_retry(err: "Failed to update #{vcs_url}") do run "hg pull" end end - private def hg_retry(err = "Failed to update repository", &) - retries = 0 - loop do - return yield - rescue ex : Error - retries += 1 - next if retries < 3 - raise Error.new("#{err}: #{ex}") - end - end - private def delete_repository Log.debug { "rm -rf #{Process.quote(local_path)}" } Shards::Helpers.rm_rf(local_path) @@ -381,95 +328,22 @@ module Shards @origin_url ||= capture("hg paths default").strip end - # Returns whether origin URLs have differing hosts and/or paths. - protected def origin_changed? - return false if origin_url == hg_url - return true if origin_url.nil? || hg_url.nil? - - origin_parsed = parse_uri(origin_url) - hg_parsed = parse_uri(hg_url) - - (origin_parsed.host != hg_parsed.host) || (origin_parsed.path != hg_parsed.path) - end - - # Parses a URI string, with additional support for ssh+git URI schemes. - 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 : HgRef, path) run("hg files -r #{Process.quote(ref.to_hg_revset)} -- #{Process.quote(path)}", raise_on_fail: false) end - private def capture(command, path = local_path) - run(command, capture: true, path: path).as(String) - end - - private def run(command, path = local_path, capture = false, raise_on_fail = true) - if Shards.local? && !Dir.exists?(path) - dependency_name = File.basename(path) - 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, raise_on_fail: raise_on_fail) - end - end - - private def run_in_current_folder(command, capture = false, raise_on_fail = true) - unless HgResolver.has_hg_command? + private def error_if_command_is_missing + unless HgResolver.command? raise Error.new("Error missing hg command line tool. Please install Mercurial first!") end - - Log.debug { command } - - 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? - if capture - output.to_s - else - true - end - elsif raise_on_fail - str = error.to_s - if str.starts_with?("abort: ") && (idx = str.index('\n')) - message = str[7...idx] - raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") - else - raise Error.new("Failed #{command}.\n#{str}") - end - end end - def report_version(version : Version) : String - hg_version = parse_hg_version(version) - if commit = hg_version.commit - "#{hg_version.value} at #{commit[0...7]}" + private def error_for_run_failure(command, str : String) + if str.starts_with?("abort: ") && (idx = str.index('\n')) + message = str[7...idx] + raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") else - version.value + raise Error.new("Failed #{command}.\n#{str}") end end diff --git a/src/resolvers/version_control.cr b/src/resolvers/version_control.cr new file mode 100644 index 00000000..8c46a7da --- /dev/null +++ b/src/resolvers/version_control.cr @@ -0,0 +1,142 @@ +require "./resolver" +require "../logger" + +module Shards + abstract class VersionControlResolver < Resolver + @@command : Bool? + @@version : String? + @origin_url : String? + + abstract def read_spec(version : Version) : String? + abstract def versions_from_tags + + def available_releases : Array(Version) + update_local_cache + versions_from_tags + end + + def vcs_url + source.strip + end + + # Retry loop for version-control commands + private def vcs_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 + + # Returns whether origin URLs have differing hosts and/or paths. + protected def origin_changed? + return false if origin_url == vcs_url + return true if origin_url.nil? || vcs_url.nil? + + origin_parsed = parse_uri(origin_url) + vcs_parsed = parse_uri(vcs_url) + + (origin_parsed.host != vcs_parsed.host) || (origin_parsed.path != vcs_parsed.path) + end + + # Parses a URI string, with additional support for ssh+git URI schemes. + 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 + + def update_local_cache + if cloned_repository? && origin_changed? + delete_repository + @updated_cache = false + end + + return if Shards.local? || @updated_cache + Log.info { "Fetching #{vcs_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 + end + + @updated_cache = true + end + + def report_version(version : Version) : String + version = parse_version(version) + if commit = version.commit + "#{version.value} at #{commit[0...7]}" + else + version.value + end + end + + private def capture(command, path = local_path) + run(command, capture: true, path: path).as(String) + end + + private def run(command, path = local_path, capture = false, raise_on_fail = true) + if Shards.local? && !Dir.exists?(path) + dependency_name = File.basename(path, @@extension) + raise Error.new("Missing repository cache for #{dependency_name.inspect}. Please run without --local to fetch it.") + end + run_in_folder(command, path, capture, raise_on_fail: raise_on_fail) + end + + # Chdir to a folder and run command. + # Runs in current folder if `path` is nil. + private def run_in_folder(command, path : String? = nil, capture = false, raise_on_fail = true) + error_if_command_is_missing + Log.debug { command } + + STDERR.flush # from fossil version, but presumably ok for git/hg + output = capture ? IO::Memory.new : Process::Redirect::Close + error = IO::Memory.new + status = Process.run(command, shell: true, output: output, error: error, chdir: path) + + if status.success? + # output.to_s if capture + if capture + output.to_s + else + true + end + elsif raise_on_fail + error_for_run_failure(command, error.to_s) + end + end + end +end