From b11bb0813ea55f26544b653782fb3afec5503bf0 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sat, 2 Jan 2021 22:16:17 +0100 Subject: [PATCH 01/22] Add first implementation of a mercurial resolver --- src/config.cr | 1 + src/resolvers/hg.cr | 463 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 464 insertions(+) create mode 100644 src/resolvers/hg.cr diff --git a/src/config.cr b/src/config.cr index e41d070e..a1cbfd9a 100644 --- a/src/config.cr +++ b/src/config.cr @@ -12,6 +12,7 @@ module Shards VERSION_REFERENCE = /^v?\d+[-.][-.a-zA-Z\d]+$/ VERSION_TAG = /^v(\d+[-.][-.a-zA-Z\d]+)$/ VERSION_AT_GIT_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+git\.commit\.([0-9a-f]+)$/ + VERSION_AT_HG_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+hg\.commit\.([0-9a-f]+)$/ def self.cache_path @@cache_path ||= find_or_create_cache_path diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr new file mode 100644 index 00000000..8e35e94d --- /dev/null +++ b/src/resolvers/hg.cr @@ -0,0 +1,463 @@ +require "uri" +require "./resolver" +require "../versions" +require "../logger" +require "../helpers" + +module Shards + abstract struct HgRef < Ref + def full_info + to_s + end + end + + struct HgBranchRef < HgRef + def initialize(@branch : String) + end + + def to_hg_ref(simple = false) + simple ? @branch : "branch(\"#{@branch}\") and head()" + end + + def to_s(io) + io << "branch " << @branch + end + + def to_yaml(yaml) + yaml.scalar "branch" + yaml.scalar @branch + end + end + + struct HgBookmarkRef < HgRef + def initialize(@bookmark : String) + end + + def to_hg_ref(simple = false) + simple ? @bookmark : "bookmark(\"#{@bookmark}\")" + end + + def to_s(io) + io << "bookmark " << @bookmark + end + + def to_yaml(yaml) + yaml.scalar "bookmark" + yaml.scalar @bookmark + end + end + + struct HgTagRef < HgRef + def initialize(@tag : String) + end + + def to_hg_ref(simple = false) + simple ? @tag : "tag(\"#{@tag}\")" + end + + def to_s(io) + io << "tag " << @tag + end + + def to_yaml(yaml) + yaml.scalar "tag" + yaml.scalar @tag + end + end + + struct HgCommitRef < HgRef + getter commit : String + + def initialize(@commit : String) + end + + def =~(other : HgCommitRef) + commit.starts_with?(other.commit) || other.commit.starts_with?(commit) + end + + def to_hg_ref(simple = false) + @commit + end + + def to_s(io) + io << "commit " << @commit[0...7] + end + + def full_info + "commit #{@commit}" + end + + def to_yaml(yaml) + yaml.scalar "commit" + yaml.scalar @commit + end + end + + struct HgCurrentRef < HgRef + def to_hg_ref(simple = false) + "." + end + + def to_s(io) + io << "current" + end + + def to_yaml(yaml) + raise NotImplementedError.new("HgCurrentRef is for internal use only") + end + end + + class HgResolver < Resolver + @@has_hg_command : Bool? + @@hg_version : String? + + @origin_url : String? + + def self.key + "hg" + end + + def self.normalize_key_source(key : String, source : String) : {String, String} + case key + when "hg" + {"hg", source} + else + raise "Unknown resolver #{key}" + end + end + + protected def self.has_hg_command? + if @@has_hg_command.nil? + @@has_hg_command = (Process.run("hg", ["--version"]).success? rescue false) + end + @@has_hg_command + end + + protected def self.hg_version + @@hg_version ||= `hg --version`[/\(version\s+([^)]*)\)/, 1] + end + + def read_spec(version : Version) : String? + update_local_cache + ref = hg_ref(version) + + if file_exists?(ref, SPEC_FILENAME) + capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + else + Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } + nil + end + end + + private def spec_at_ref(ref : HgRef) : Spec? + update_local_cache + begin + if file_exists?(ref, SPEC_FILENAME) + spec_yaml = capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + Spec.from_yaml(spec_yaml) + end + rescue Error + nil + 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 + begin + commit = commit_sha1_at(ref) + rescue Error + raise Error.new "Could not find #{ref.full_info} for shard #{name.inspect} in the repository #{source}" + end + + if spec = spec_at_ref(ref) + Version.new "#{spec.version.value}+hg.commit.#{commit}" + else + raise Error.new "No #{SPEC_FILENAME} was found for shard #{name.inspect} at commit #{commit}" + end + end + + def matches_ref?(ref : HgRef, version : Version) + case ref + when HgCommitRef + ref =~ hg_ref(version) + when HgBranchRef, HgBookmarkRef, HgCurrentRef + # TODO: check if version is the branch + version.has_metadata? + else + # TODO: check branch and tags + true + end + end + + protected def versions_from_tags + capture("hg tags --template '{tag}\\n'") + .split('\n') + .sort! + .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } + end + + def matches?(commit) + if branch = dependency["branch"]? + !capture("hg log -r 'branch(#{tag}) and descendants(#{commit})' --template '{branch}\\n'").strip.empty? + elsif bookmark = dependency["bookmark"]? + !capture("hg log -r 'bookmark(#{bookmark}) and descendants(#{commit})' --template '{bookmarks}\\n'").strip.empty? + elsif tag = dependency["tag"]? + !capture("hg log -r 'tag(#{tag}) and descendants(#{commit})' --template '{tags}\n'").strip.empty? + else + !capture("hg log -r #{commit}").strip.empty? + end + end + + def install_sources(version : Version, install_path : String) + update_local_cache + ref = hg_ref(version) + + FileUtils.rm_r(install_path) if File.exists?(install_path) + Dir.mkdir_p(install_path) + run "hg clone --quiet -u #{Process.quote(ref.to_hg_ref(true))} -- #{Process.quote(local_path)} #{Process.quote(install_path)}" + end + + def commit_sha1_at(ref : HgRef) + capture("hg log -r #{Process.quote(ref.to_hg_ref)} --template '{node}'").strip + end + + def local_path + @local_path ||= begin + uri = parse_uri(hg_url) + + path = uri.path + path = Path[path] + # E.g. turns "c:\local\path" into "c\local\path". Or just drops the leading slash. + if (anchor = path.anchor) + path = Path[path.drive.to_s.rchop(":"), path.relative_to(anchor)] + end + + if host = uri.host + File.join(Shards.cache_path, host, path) + else + File.join(Shards.cache_path, path) + end + end + end + + def hg_url + source.strip + end + + def parse_requirement(params : Hash(String, String)) : Requirement + params.each do |key, value| + case key + when "branch" + return HgBranchRef.new value + when "bookmark" + return HgBookmarkRef.new value + when "tag" + return HgTagRef.new value + when "commit" + return HgCommitRef.new value + else + end + end + + super + end + + record HgVersion, value : String, commit : String? = nil + + private def parse_hg_version(version : Version) : HgVersion + case version.value + when VERSION_REFERENCE + HgVersion.new version.value + when VERSION_AT_HG_COMMIT + HgVersion.new $1, $2 + else + raise Error.new("Invalid version for hg resolver: #{version}") + end + end + + private def hg_ref(version : Version) : HgRef + hg_version = parse_hg_version(version) + if commit = hg_version.commit + HgCommitRef.new commit + else + HgTagRef.new "v#{hg_version.value}" + end + end + + private 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 + hg_retry(err: "Failed to clone #{hg_url}") 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(hg_url)} #{Process.quote(local_path)}" + end + end + + private def fetch_repository + hg_retry(err: "Failed to update #{hg_url}") do + run "hg pull" + end + end + + private def hg_retry(err = "Failed to update repository") + retries = 0 + loop do + yield + break + rescue Error + retries += 1 + next if retries < 3 + raise Error.new(err) + end + end + + private def delete_repository + Log.debug { "rm -rf #{Process.quote(local_path)}'" } + Shards::Helpers.rm_rf(local_path) + @origin_url = nil + end + + private def cloned_repository? + Dir.exists?(local_path) + end + + private def valid_repository? + File.each_line(File.join(local_path, ".hg", "dirstate")) do |line| + return true if line =~ /mirror\s*=\s*true/ + end + false + end + + private def origin_url + @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) + files = capture("hg files -r #{Process.quote(ref.to_hg_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) + 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 HgResolver.has_hg_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? + output.to_s if capture + else + str = error.to_s + if str.starts_with?("abort: ") && (idx = str.index('\n')) + message = str[7...idx] + else + message = str + end + raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") + 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]}" + else + version.value + end + end + + register_resolver "hg", HgResolver + end +end From c1a6c2dca8da5de217cdefdbd8e0c2ca78d5227e Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sat, 2 Jan 2021 22:21:48 +0100 Subject: [PATCH 02/22] Add specs for hg resolver --- spec/support/factories.cr | 86 +++++++++++++++++++ spec/support/requirement.cr | 4 + spec/unit/hg_resolver_spec.cr | 153 ++++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 spec/unit/hg_resolver_spec.cr diff --git a/spec/support/factories.cr b/spec/support/factories.cr index af800bcb..c15da9fe 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -80,6 +80,78 @@ def checkout_git_branch(project, branch) end end +def create_hg_repository(project, *versions) + Dir.cd(tmp_path) do + run "hg init #{Process.quote(project)}" + end + + Dir.mkdir(File.join(hg_path(project), "src")) + File.write(File.join(hg_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") + + Dir.cd(hg_path(project)) do + run "hg add #{Process.quote("src/#{project}.cr")}" + end + + versions.each { |version| create_hg_release project, version } +end + +def create_fork_hg_repository(project, upstream) + Dir.cd(tmp_path) do + run "hg clone #{Process.quote(hg_url(upstream))} #{Process.quote(project)}" + end +end + +def create_hg_version_commit(project, version, shard : Bool | NamedTuple = true) + Dir.cd(hg_path(project)) do + if shard + contents = shard.is_a?(NamedTuple) ? shard : nil + create_shard project, version, contents + end + Dir.cd(hg_path(project)) do + name = shard[:name]? if shard.is_a?(NamedTuple) + name ||= project + File.touch "src/#{name}.cr" + run "hg add #{Process.quote("src/#{name}.cr")}" + end + create_hg_commit project, "release: v#{version}" + end +end + +def create_hg_release(project, version, shard : Bool | NamedTuple = true) + create_hg_version_commit(project, version, shard) + create_hg_tag(project, "v#{version}") +end + +def create_hg_tag(project, version) + Dir.cd(hg_path(project)) do + run "hg tag #{Process.quote(version)}" + end +end + +def create_hg_commit(project, message = "new commit") + Dir.cd(hg_path(project)) do + run "hg commit -A -m #{Process.quote(message)}" + end +end + +def checkout_new_hg_bookmark(project, branch) + Dir.cd(hg_path(project)) do + run "hg bookmark #{Process.quote(branch)}" + end +end + +def checkout_new_hg_branch(project, branch) + Dir.cd(hg_path(project)) do + run "hg branch #{Process.quote(branch)}" + end +end + +def checkout_hg_rev(project, rev) + Dir.cd(hg_path(project)) do + run "hg update -C #{Process.quote(rev)}" + end +end + def create_shard(project, version, contents : NamedTuple? = nil) spec = {name: project, version: version, crystal: Shards.crystal_version} spec = spec.merge(contents) if contents @@ -116,6 +188,20 @@ def git_path(project) File.join(tmp_path, project.to_s) end +def hg_commits(project, rev = ".") + Dir.cd(hg_path(project)) do + run("hg log --template='{node}\n' -r #{Process.quote(rev)}").strip.split('\n') + end +end + +def hg_url(project) + "file://#{Path[hg_path(project)].to_posix}" +end + +def hg_path(project) + File.join(tmp_path, project.to_s) +end + def rel_path(project) "../../spec/.repositories/#{project}" end diff --git a/spec/support/requirement.cr b/spec/support/requirement.cr index 7a7a8223..24594575 100644 --- a/spec/support/requirement.cr +++ b/spec/support/requirement.cr @@ -6,6 +6,10 @@ def commit(sha1) Shards::GitCommitRef.new(sha1) end +def hg_branch(name) + Shards::HgBranchRef.new(name) +end + def version(version) Shards::Version.new(version) end diff --git a/spec/unit/hg_resolver_spec.cr b/spec/unit/hg_resolver_spec.cr new file mode 100644 index 00000000..66b99821 --- /dev/null +++ b/spec/unit/hg_resolver_spec.cr @@ -0,0 +1,153 @@ +require "./spec_helper" + +private def resolver(name) + Shards::HgResolver.new(name, hg_url(name)) +end + +module Shards + # Allow overriding `source` for the specs + class HgResolver + def source=(@source) + end + end + + describe HgResolver do + before_each do + create_hg_repository "empty" + create_hg_commit "empty", "initial release" + + create_hg_repository "unreleased" + create_hg_version_commit "unreleased", "0.1.0" + checkout_new_hg_branch "unreleased", "branch" + create_hg_commit "unreleased", "testing" + checkout_hg_rev "unreleased", "default" + + create_hg_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0" + + # Create a version tag not prefixed by 'v' which should be ignored + create_hg_tag "library", "99.9.9" + end + + it "available releases" do + resolver("empty").available_releases.should be_empty + resolver("library").available_releases.should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) + end + + it "latest version for ref" do + expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do + resolver("empty").latest_version_for_ref(hg_branch "default") + end + expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do + resolver("empty").latest_version_for_ref(nil) + end + resolver("unreleased").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") + resolver("unreleased").latest_version_for_ref(hg_branch "branch").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased, "branch")[0]}") + resolver("unreleased").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") + resolver("library").latest_version_for_ref(hg_branch "default").should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") + resolver("library").latest_version_for_ref(nil).should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") + expect_raises(Shards::Error, "Could not find branch foo for shard \"library\" in the repository #{hg_url(:library)}") do + resolver("library").latest_version_for_ref(hg_branch "foo") + end + end + + it "versions for" do + expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do + resolver("empty").versions_for(Any) + end + resolver("library").versions_for(Any).should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"]) + resolver("library").versions_for(VersionReq.new "~> 0.1.0").should eq(versions ["0.1.0", "0.1.1", "0.1.2"]) + resolver("library").versions_for(hg_branch "default").should eq(versions ["0.2.0+hg.commit.#{hg_commits(:library)[0]}"]) + resolver("unreleased").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) + resolver("unreleased").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) + end + + it "read spec for release" do + spec = resolver("library").spec(version "0.1.1") + spec.original_version.should eq(version "0.1.1") + spec.version.should eq(version "0.1.1") + end + + it "read spec for commit" do + version = version("0.2.0+hg.commit.#{hg_commits(:library)[0]}") + spec = resolver("library").spec(version) + spec.original_version.should eq(version "0.2.0") + spec.version.should eq(version) + end + + it "install" do + library = resolver("library") + + library.install_sources(version("0.1.2"), install_path("library")) + File.exists?(install_path("library", "src/library.cr")).should be_true + File.exists?(install_path("library", "shard.yml")).should be_true + Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.1.2") + + library.install_sources(version("0.2.0"), install_path("library")) + Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") + end + + it "install commit" do + library = resolver("library") + version = version "0.2.0+hg.commit.#{hg_commits(:library)[0]}" + library.install_sources(version, install_path("library")) + Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0") + end + + it "origin changed" do + library = HgResolver.new("library", hg_url("library")) + library.install_sources(version("0.1.2"), install_path("library")) + + # Change the origin in the cache repo to https://foss.heptapod.net/foo/bar + hgrc_path = File.join(library.local_path, ".hg", "hgrc") + hgrc = File.read(hgrc_path) + hgrc = hgrc.gsub(/(default\s*=\s*)([^\r\n]*)/, "\\1https://foss.heptapod.net/foo/bar") + File.write(hgrc_path, hgrc) + # + # All of these alternatives should not trigger origin as changed + same_origins = [ + "https://foss.heptapod.net/foo/bar", + "https://foss.heptapod.net:1234/foo/bar", + "http://foss.heptapod.net/foo/bar", + "ssh://foss.heptapod.net/foo/bar", + "hg://foss.heptapod.net/foo/bar", + "rsync://foss.heptapod.net/foo/bar", + "hg@foss.heptapod.net:foo/bar", + "bob@foss.heptapod.net:foo/bar", + "foss.heptapod.net:foo/bar", + ] + + same_origins.each do |origin| + library.source = origin + library.origin_changed?.should be_false + end + + # These alternatives should all trigger origin as changed + changed_origins = [ + "https://foss.heptapod.net/foo/bar2", + "https://foss.heptapod.net/foos/bar", + "https://hghubz.com/foo/bar", + "file:///foss.heptapod.net/foo/bar", + "hg@foss.heptapod.net:foo/bar2", + "hg@foss.heptapod2.net.com:foo/bar", + "", + ] + + changed_origins.each do |origin| + library.source = origin + library.origin_changed?.should be_true + end + end + + it "renders report version" do + resolver("library").report_version(version "1.2.3").should eq("1.2.3") + resolver("library").report_version(version "1.2.3+hg.commit.654875c9dbfa8d72fba70d65fd548d51ffb85aff").should eq("1.2.3 at 654875c") + end + + it "#matches_ref" do + resolver = HgResolver.new("", "") + resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567")).should be_true + resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true + resolver.matches_ref?(HgCommitRef.new("1234567"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true + end + end +end From 976260c9219af479c5b7bc3c8da2be0d90cb3a9e Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sat, 2 Jan 2021 22:40:34 +0100 Subject: [PATCH 03/22] Add some specs for mercurial bookmarks --- spec/support/factories.cr | 1 + spec/support/requirement.cr | 4 ++++ spec/unit/hg_resolver_spec.cr | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/spec/support/factories.cr b/spec/support/factories.cr index c15da9fe..2766d014 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -130,6 +130,7 @@ end def create_hg_commit(project, message = "new commit") Dir.cd(hg_path(project)) do + File.write("src/#{project}.cr", "# #{message}", mode: "a") run "hg commit -A -m #{Process.quote(message)}" end end diff --git a/spec/support/requirement.cr b/spec/support/requirement.cr index 24594575..e308dbde 100644 --- a/spec/support/requirement.cr +++ b/spec/support/requirement.cr @@ -6,6 +6,10 @@ def commit(sha1) Shards::GitCommitRef.new(sha1) end +def hg_bookmark(name) + Shards::HgBookmarkRef.new(name) +end + def hg_branch(name) Shards::HgBranchRef.new(name) end diff --git a/spec/unit/hg_resolver_spec.cr b/spec/unit/hg_resolver_spec.cr index 66b99821..97df5ef5 100644 --- a/spec/unit/hg_resolver_spec.cr +++ b/spec/unit/hg_resolver_spec.cr @@ -22,6 +22,12 @@ module Shards create_hg_commit "unreleased", "testing" checkout_hg_rev "unreleased", "default" + create_hg_repository "unreleased-bm" + create_hg_version_commit "unreleased-bm", "0.1.0" + checkout_new_hg_bookmark "unreleased-bm", "branch" + create_hg_commit "unreleased-bm", "testing" + checkout_hg_rev "unreleased-bm", "default" + create_hg_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0" # Create a version tag not prefixed by 'v' which should be ignored @@ -43,6 +49,9 @@ module Shards resolver("unreleased").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") resolver("unreleased").latest_version_for_ref(hg_branch "branch").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased, "branch")[0]}") resolver("unreleased").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}") + resolver("unreleased-bm").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}") + resolver("unreleased-bm").latest_version_for_ref(hg_bookmark "branch").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm", "branch")[0]}") + resolver("unreleased-bm").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}") resolver("library").latest_version_for_ref(hg_branch "default").should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") resolver("library").latest_version_for_ref(nil).should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}") expect_raises(Shards::Error, "Could not find branch foo for shard \"library\" in the repository #{hg_url(:library)}") do @@ -59,6 +68,8 @@ module Shards resolver("library").versions_for(hg_branch "default").should eq(versions ["0.2.0+hg.commit.#{hg_commits(:library)[0]}"]) resolver("unreleased").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) resolver("unreleased").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"]) + resolver("unreleased-bm").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"]) + resolver("unreleased-bm").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"]) end it "read spec for release" do From efdeda70859cf9ae733c1930ee499ce7c969ae8d Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 10:45:07 +0100 Subject: [PATCH 04/22] HgResolver uses the Mercurial command server --- src/resolvers/hg.cr | 147 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 122 insertions(+), 25 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 8e35e94d..fdc45a70 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -110,6 +110,7 @@ module Shards class HgResolver < Resolver @@has_hg_command : Bool? @@hg_version : String? + @@hg_process : Process | Bool | Nil @origin_url : String? @@ -134,7 +135,7 @@ module Shards end protected def self.hg_version - @@hg_version ||= `hg --version`[/\(version\s+([^)]*)\)/, 1] + @@hg_version ||= hg("--version")[/\(version\s+([^)]*)\)/, 1] end def read_spec(version : Version) : String? @@ -142,7 +143,7 @@ module Shards ref = hg_ref(version) if file_exists?(ref, SPEC_FILENAME) - capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + capture_hg("cat", "-r", ref.to_hg_ref, SPEC_FILENAME) else Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } nil @@ -153,7 +154,7 @@ module Shards update_local_cache begin if file_exists?(ref, SPEC_FILENAME) - spec_yaml = capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + spec_yaml = capture_hg("cat", "-r", ref.to_hg_ref, SPEC_FILENAME) Spec.from_yaml(spec_yaml) end rescue Error @@ -201,7 +202,7 @@ module Shards end protected def versions_from_tags - capture("hg tags --template '{tag}\\n'") + capture_hg("tags", "--template", "{tag}\\n") .split('\n') .sort! .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } @@ -209,13 +210,13 @@ module Shards def matches?(commit) if branch = dependency["branch"]? - !capture("hg log -r 'branch(#{tag}) and descendants(#{commit})' --template '{branch}\\n'").strip.empty? + !capture_hg("log", "-r", "branch(#{tag}) and descendants(#{commit})", "--template", "{branch}\\n").strip.empty? elsif bookmark = dependency["bookmark"]? - !capture("hg log -r 'bookmark(#{bookmark}) and descendants(#{commit})' --template '{bookmarks}\\n'").strip.empty? + !capture_hg("hg", "log", "-r", "bookmark(#{bookmark}) and descendants(#{commit})", "--template", "{bookmarks}\\n").strip.empty? elsif tag = dependency["tag"]? - !capture("hg log -r 'tag(#{tag}) and descendants(#{commit})' --template '{tags}\n'").strip.empty? + !capture_hg("hg", "log", "-r", "tag(#{tag}) and descendants(#{commit})", "--template", "{tags}\n").strip.empty? else - !capture("hg log -r #{commit}").strip.empty? + !capture_hg("log", "-r", commit).strip.empty? end end @@ -224,12 +225,11 @@ module Shards ref = hg_ref(version) FileUtils.rm_r(install_path) if File.exists?(install_path) - Dir.mkdir_p(install_path) - run "hg clone --quiet -u #{Process.quote(ref.to_hg_ref(true))} -- #{Process.quote(local_path)} #{Process.quote(install_path)}" + run_hg "clone", "--quiet", "-u", ref.to_hg_ref(true), local_path, install_path, path: nil end def commit_sha1_at(ref : HgRef) - capture("hg log -r #{Process.quote(ref.to_hg_ref)} --template '{node}'").strip + capture_hg("log", "-r", ref.to_hg_ref, "--template", "{node}").strip end def local_path @@ -326,13 +326,13 @@ module Shards # # 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(hg_url)} #{Process.quote(local_path)}" + run_hg "clone", hg_url, local_path, path: nil end end private def fetch_repository hg_retry(err: "Failed to update #{hg_url}") do - run "hg pull" + run_hg "pull" end end @@ -366,7 +366,7 @@ module Shards end private def origin_url - @origin_url ||= capture("hg paths default").strip + @origin_url ||= capture_hg("paths", "default").strip end # Returns whether origin URLs have differing hosts and/or paths. @@ -407,34 +407,131 @@ module Shards end private def file_exists?(ref : HgRef, path) - files = capture("hg files -r #{Process.quote(ref.to_hg_ref)} -- #{Process.quote(path)}") + files = capture_hg("files", "-r", ref.to_hg_ref, path) !files.strip.empty? end - private def capture(command, path = local_path) - run(command, capture: true, path: path).not_nil! + private def capture_hg(*args, path = local_path) + run_hg(*args, capture: true, path: path).not_nil! end - private def run(command, path = local_path, capture = false) - if Shards.local? && !Dir.exists?(path) + private def run_hg(*args, path = local_path, capture = false) + if path && 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) + HgResolver.hg(*args, path: path, capture: capture) + end + + # Execute a hg command in the given path + # + # The command is run through the hg command server (if available) and + # the command line tool otherwise. The command server is started if the + # function is called for the first time. + def self.hg(*args, path = Dir.current, capture = true) + unless process = @@hg_process + Log.debug { "Start Mercurial command server" } + + process = Process.new("hg", + ["serve", "--cmdserver", "pipe"], + env: {"HGENCODING" => "UTF-8"}, # enforce UTF-8 encoding + shell: true, + input: Process::Redirect::Pipe, + output: Process::Redirect::Pipe, + error: Process::Redirect::Inherit) + @@hg_process = process + + output = process.output + # Read the hello block + channel = output.read_byte + len = output.read_bytes(UInt32, IO::ByteFormat::BigEndian) + hello = output.read_string(len) + + Log.debug { "Mercurial command server hello: #{hello}" } + + # Verify that the command server uses UTF-8 encoding + if encoding = hello.each_line.map(&.split(": ")).find(&.[0].== "encoding") + if encoding[1] != "UTF-8" + # actually, this should *never* happen + Log.warn { "Mercurial command server does not use UTF-8 encoding (#{encoding[1]}), fallback to direct command" } + @@hg_process = true + end + end + end + + # Do not use the command server but run the command directly + if process.is_a?(Bool) + if path + return Dir.cd(path) { run_in_current_folder(*args, capture: capture) } + else + return run_in_current_folder(*args, capture: capture) + end + end + + # Use the command server + cmd = String.build do |b| + # Run the command in the specified directory + b << "--cwd" << "\0" << path << "\0" if path + b << args.each.join("\0") + end + + input = process.input + output = process.output + + input.write("runcommand\n".to_slice) + input.write_bytes(cmd.bytesize, IO::ByteFormat::BigEndian) + input.write(cmd.to_slice) + + result = capture ? String::Builder.new : nil + error_msg = "" + status = 0 + while true + channel = output.read_byte + len = output.read_bytes(UInt32, IO::ByteFormat::BigEndian) + + case channel + when 'o' + if result + result << output.read_string(len) + else + output.read_string(len) + end + when 'e' + error_msg = output.read_string(len) + when 'r' + status = output.read_bytes(Int32) + break + when 'L' + raise Error.new("Mercurial process expects a line input") + when 'I' + raise Error.new("Mercurial process expects a block input") + end + end + + if status.zero? + result.to_s if result + else + str = error_msg.to_s + if str.starts_with?("abort: ") && (idx = str.index('\n')) + message = str[7...idx] + else + message = str + end + raise Error.new("Failed hg #{args.join(" ")} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") end end - private def run_in_current_folder(command, capture = false) + # Run the hg command line tool with some command line args in the current folder + private def self.run_in_current_folder(*args, capture = false) unless HgResolver.has_hg_command? raise Error.new("Error missing hg command line tool. Please install Mercurial first!") end - Log.debug { command } + Log.debug { "hg #{args.join(" ")}" } output = capture ? IO::Memory.new : Process::Redirect::Close error = IO::Memory.new - status = Process.run(command, shell: true, output: output, error: error) + status = Process.run("hg", args, shell: true, output: output, error: error) if status.success? output.to_s if capture @@ -445,7 +542,7 @@ module Shards else message = str end - raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch, bookmark or file doesn't exist?") + raise Error.new("Failed hg #{args.join(" ")} (#{message}). Maybe a commit, branch or file doesn't exist?") end end From 3da7e526e0e88235c06e835e35aa0eb33605d555 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 10:14:40 +0100 Subject: [PATCH 05/22] Specs for HgResolver use the command server --- spec/support/factories.cr | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/support/factories.cr b/spec/support/factories.cr index 2766d014..8f1f222b 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -82,14 +82,14 @@ end def create_hg_repository(project, *versions) Dir.cd(tmp_path) do - run "hg init #{Process.quote(project)}" + Shards::HgResolver.hg "init", project end Dir.mkdir(File.join(hg_path(project), "src")) File.write(File.join(hg_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend") Dir.cd(hg_path(project)) do - run "hg add #{Process.quote("src/#{project}.cr")}" + Shards::HgResolver.hg "add", "src/#{project}.cr" end versions.each { |version| create_hg_release project, version } @@ -97,7 +97,7 @@ end def create_fork_hg_repository(project, upstream) Dir.cd(tmp_path) do - run "hg clone #{Process.quote(hg_url(upstream))} #{Process.quote(project)}" + Shards::HgResolver.hg "clone", hg_url(upstream), project end end @@ -111,7 +111,7 @@ def create_hg_version_commit(project, version, shard : Bool | NamedTuple = true) name = shard[:name]? if shard.is_a?(NamedTuple) name ||= project File.touch "src/#{name}.cr" - run "hg add #{Process.quote("src/#{name}.cr")}" + Shards::HgResolver.hg "add", "src/#{name}.cr" end create_hg_commit project, "release: v#{version}" end @@ -124,32 +124,32 @@ end def create_hg_tag(project, version) Dir.cd(hg_path(project)) do - run "hg tag #{Process.quote(version)}" + Shards::HgResolver.hg "tag", version end end def create_hg_commit(project, message = "new commit") Dir.cd(hg_path(project)) do File.write("src/#{project}.cr", "# #{message}", mode: "a") - run "hg commit -A -m #{Process.quote(message)}" + Shards::HgResolver.hg "commit", "-A", "-m", message end end def checkout_new_hg_bookmark(project, branch) Dir.cd(hg_path(project)) do - run "hg bookmark #{Process.quote(branch)}" + Shards::HgResolver.hg "bookmark", branch end end def checkout_new_hg_branch(project, branch) Dir.cd(hg_path(project)) do - run "hg branch #{Process.quote(branch)}" + Shards::HgResolver.hg "branch", branch end end def checkout_hg_rev(project, rev) Dir.cd(hg_path(project)) do - run "hg update -C #{Process.quote(rev)}" + Shards::HgResolver.hg "update", "-C", rev end end @@ -191,7 +191,7 @@ end def hg_commits(project, rev = ".") Dir.cd(hg_path(project)) do - run("hg log --template='{node}\n' -r #{Process.quote(rev)}").strip.split('\n') + Shards::HgResolver.hg("log", "--template", "{node}\n", "-r", rev).not_nil!.strip.split('\n') end end From 8b4fd1a260872819aa4d245f49639805adcf85e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 3 Jan 2021 15:50:13 +0100 Subject: [PATCH 06/22] [CI] Install mercurial --- .circleci/config.yml | 10 ++++++++-- .travis.yml | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ac1bc4e..be0d5ae2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -40,6 +40,9 @@ jobs: docker: - image: crystallang/crystal:latest steps: + - run: + name: Install mercurial + command: apt-get update && apt-get install mercurial -y - shards-make-test test-on-osx: @@ -49,14 +52,17 @@ jobs: - with-brew-cache: steps: - run: - name: Install Crystal - command: brew install crystal + name: Install Crystal and Mercurial + command: brew install crystal mercurial - shards-make-test test-on-nightly: docker: - image: crystallang/crystal:nightly steps: + - run: + name: Install mercurial + command: apt-get update && apt-get install mercurial -y - shards-make-test workflows: diff --git a/.travis.yml b/.travis.yml index 45f5a148..039a7409 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,6 @@ before_script: | git config --global column.ui always script: + - apt-get update && apt-get install mercurial -y - make test - crystal tool format --check src spec From d32e82a6302103eac1aa1215593432a9e1920a12 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 16:11:06 +0100 Subject: [PATCH 07/22] Forward error message in `hg_retry` --- src/resolvers/hg.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 8e35e94d..513c1414 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -341,10 +341,10 @@ module Shards loop do yield break - rescue Error + rescue ex : Error retries += 1 next if retries < 3 - raise Error.new(err) + raise Error.new("#{err}: #{ex}") end end From 736b656deebcc378b387f69a6723c4331a2be094 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 16:24:56 +0100 Subject: [PATCH 08/22] Ensure parent directory exists before hg clone --- src/resolvers/hg.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 513c1414..1a73a091 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -321,12 +321,14 @@ module Shards end private def mirror_repository + path = local_path + Dir.mkdir_p(File.dirname(path)) hg_retry(err: "Failed to clone #{hg_url}") 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(hg_url)} #{Process.quote(local_path)}" + run_in_current_folder "hg clone --quiet -- #{Process.quote(hg_url)} #{Process.quote(path)}" end end From fa972636a6be2899d0564bd20fa37500639c3490 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 16:49:13 +0100 Subject: [PATCH 09/22] Ensure target path exists when installing sources from hg --- src/resolvers/hg.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index ab6e0236..78c07623 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -225,6 +225,7 @@ module Shards ref = hg_ref(version) FileUtils.rm_r(install_path) if File.exists?(install_path) + Dir.mkdir_p(install_path) run_hg "clone", "--quiet", "-u", ref.to_hg_ref(true), local_path, install_path, path: nil end From 16da02b480f8653f62816f1bb54baf657fa0817d Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Sun, 3 Jan 2021 18:31:21 +0100 Subject: [PATCH 10/22] Remove install path before installing sources from hg --- src/resolvers/hg.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 1a73a091..128f4253 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -322,7 +322,8 @@ module Shards private def mirror_repository path = local_path - Dir.mkdir_p(File.dirname(path)) + FileUtils.rm_r(path) if File.exists?(path) + Dir.mkdir_p(path) hg_retry(err: "Failed to clone #{hg_url}") do # We checkout the working directory so that "." is meaningful. # From 2cb5a69d2325642c3e9c01d82a0490728561c955 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 10:28:29 +0100 Subject: [PATCH 11/22] [CI] Fix installation of mercurial on travis-ci --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 039a7409..6dc9bbd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ language: crystal os: - linux +before_install: + - sudo apt-get update + - sudo apt-get -y install mercurial + before_script: | make git config --global user.email "you@example.com" @@ -10,6 +14,5 @@ before_script: | git config --global column.ui always script: - - apt-get update && apt-get install mercurial -y - make test - crystal tool format --check src spec From bd79e579931629d8c1c75136beca1b4194ce3001 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 11:14:56 +0100 Subject: [PATCH 12/22] [CI] Install mercurial in github workflow --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af1061cc..233940fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,15 @@ jobs: git config --global column.ui always git config --global core.autocrlf false + - name: Install Python + uses: actions/setup-python@v2 + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install Mercurial + run: pip install mercurial + - name: Install Crystal uses: oprypin/install-crystal@v1 with: From 06b9255f184e59e4404f42c0fa7a988661ec16c0 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 11:28:28 +0100 Subject: [PATCH 13/22] Call `Process` with full quoted command line Otherwise it won't work on Windows currently. --- src/resolvers/hg.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 53c84883..4634fcd9 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -436,8 +436,7 @@ module Shards unless process = @@hg_process Log.debug { "Start Mercurial command server" } - process = Process.new("hg", - ["serve", "--cmdserver", "pipe"], + process = Process.new("hg serve --cmdserver pipe", env: {"HGENCODING" => "UTF-8"}, # enforce UTF-8 encoding shell: true, input: Process::Redirect::Pipe, @@ -535,7 +534,8 @@ module Shards output = capture ? IO::Memory.new : Process::Redirect::Close error = IO::Memory.new - status = Process.run("hg", args, shell: true, output: output, error: error) + command = "hg #{args.each.map { |arg| Process.quote(arg) }.join(" ")}" + status = Process.run(command, shell: true, output: output, error: error) if status.success? output.to_s if capture @@ -546,7 +546,7 @@ module Shards else message = str end - raise Error.new("Failed hg #{args.join(" ")} (#{message}). Maybe a commit, branch or file doesn't exist?") + raise Error.new("Failed #{command} (#{message}). Maybe a commit, branch or file doesn't exist?") end end From d9e2a7b99f57a79a36d1ce5aec55dc21e1c1af91 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 12:29:40 +0100 Subject: [PATCH 14/22] Quote shell arguments correctly --- spec/support/factories.cr | 2 +- src/resolvers/hg.cr | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/support/factories.cr b/spec/support/factories.cr index 2766d014..d32b48b0 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -191,7 +191,7 @@ end def hg_commits(project, rev = ".") Dir.cd(hg_path(project)) do - run("hg log --template='{node}\n' -r #{Process.quote(rev)}").strip.split('\n') + run("hg log --template=#{Process.quote("{node}\n")} -r #{Process.quote(rev)}").strip.split('\n') end end diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 128f4253..2997babb 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -201,7 +201,7 @@ module Shards end protected def versions_from_tags - capture("hg tags --template '{tag}\\n'") + capture("hg tags --template #{Process.quote("{tag}\n")}") .split('\n') .sort! .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } @@ -209,14 +209,15 @@ module Shards def matches?(commit) if branch = dependency["branch"]? - !capture("hg log -r 'branch(#{tag}) and descendants(#{commit})' --template '{branch}\\n'").strip.empty? + rev = "branch(\"#{tag}\") and descendants(#{commit})" elsif bookmark = dependency["bookmark"]? - !capture("hg log -r 'bookmark(#{bookmark}) and descendants(#{commit})' --template '{bookmarks}\\n'").strip.empty? + rev = "bookmark(\"#{bookmark}\") and descendants(#{commit})" elsif tag = dependency["tag"]? - !capture("hg log -r 'tag(#{tag}) and descendants(#{commit})' --template '{tags}\n'").strip.empty? + rev = "tag(\"tag\") and descendants(#{commit})" else - !capture("hg log -r #{commit}").strip.empty? + rev = commit end + !capture("hg log -r #{Process.quote(rev)}").strip.empty? end def install_sources(version : Version, install_path : String) @@ -229,7 +230,7 @@ module Shards end def commit_sha1_at(ref : HgRef) - capture("hg log -r #{Process.quote(ref.to_hg_ref)} --template '{node}'").strip + capture("hg log -r #{Process.quote(ref.to_hg_ref)} --template #{Process.quote("{node}\n")}").strip end def local_path @@ -352,7 +353,7 @@ module Shards 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 From dbac5f9860a55cb425d5bd544070ebcd5c266aa3 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 11:45:28 +0100 Subject: [PATCH 15/22] Remove a `file://` prefix from urls on Windows --- src/resolvers/hg.cr | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 2997babb..93b6adbf 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -325,12 +325,18 @@ module Shards path = local_path FileUtils.rm_r(path) if File.exists?(path) Dir.mkdir_p(path) - hg_retry(err: "Failed to clone #{hg_url}") do + + source = hg_url + # Remove a "file://" from the beginning, otherwise the path might be invalid + # on Windows. + source = source[7..] if source.starts_with?("file://") + + hg_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(hg_url)} #{Process.quote(path)}" + run_in_current_folder "hg clone --quiet -- #{Process.quote(source)} #{Process.quote(path)}" end end From 16099f841ea7a7921d16301e5f2028b574c53277 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 12:14:45 +0100 Subject: [PATCH 16/22] Set explicit username for hg commit and hg tag --- spec/support/factories.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/factories.cr b/spec/support/factories.cr index d32b48b0..7cad5926 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -124,14 +124,14 @@ end def create_hg_tag(project, version) Dir.cd(hg_path(project)) do - run "hg tag #{Process.quote(version)}" + run "hg tag -u #{Process.quote("Your Name ")} #{Process.quote(version)}" end end def create_hg_commit(project, message = "new commit") Dir.cd(hg_path(project)) do File.write("src/#{project}.cr", "# #{message}", mode: "a") - run "hg commit -A -m #{Process.quote(message)}" + run "hg commit -u #{Process.quote("Your Name ")} -A -m #{Process.quote(message)}" end end From a4a2e92009635a1ede8555e10758836f7ebfc2c8 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 19:33:32 +0100 Subject: [PATCH 17/22] Only check for existence of `.hg/dirstate` for identifying hg repos --- src/resolvers/hg.cr | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 93b6adbf..86185a35 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -369,10 +369,7 @@ module Shards end private def valid_repository? - File.each_line(File.join(local_path, ".hg", "dirstate")) do |line| - return true if line =~ /mirror\s*=\s*true/ - end - false + File.exists?(File.join(local_path, ".hg", "dirstate")) end private def origin_url From 3266b2b2d07281a3ee0d4232ab6cc7a6a9f3c7ba Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 19:34:18 +0100 Subject: [PATCH 18/22] Improve code of HgResolver --- src/resolvers/hg.cr | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 86185a35..8abb75bc 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -202,7 +202,7 @@ module Shards protected def versions_from_tags capture("hg tags --template #{Process.quote("{tag}\n")}") - .split('\n') + .lines .sort! .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } end @@ -267,7 +267,6 @@ module Shards return HgTagRef.new value when "commit" return HgCommitRef.new value - else end end @@ -329,7 +328,7 @@ module Shards source = hg_url # Remove a "file://" from the beginning, otherwise the path might be invalid # on Windows. - source = source[7..] if source.starts_with?("file://") + source = source.lchop("file://") hg_retry(err: "Failed to clone #{source}") do # We checkout the working directory so that "." is meaningful. @@ -349,8 +348,7 @@ module Shards private def hg_retry(err = "Failed to update repository") retries = 0 loop do - yield - break + return yield rescue ex : Error retries += 1 next if retries < 3 From 7afdb5038268e692396891e9a3149dba0715329e Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Mon, 4 Jan 2021 20:25:12 +0100 Subject: [PATCH 19/22] Add `to_hg_revset` and `to_hg_ref` methods --- src/resolvers/hg.cr | 46 ++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 8abb75bc..bfa829ee 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -15,8 +15,12 @@ module Shards def initialize(@branch : String) end - def to_hg_ref(simple = false) - simple ? @branch : "branch(\"#{@branch}\") and head()" + def to_hg_ref + @branch + end + + def to_hg_revset + "branch(\"#{@branch}\") and head()" end def to_s(io) @@ -33,8 +37,12 @@ module Shards def initialize(@bookmark : String) end - def to_hg_ref(simple = false) - simple ? @bookmark : "bookmark(\"#{@bookmark}\")" + def to_hg_ref + @bookmark + end + + def to_hg_revset + "bookmark(\"#{@bookmark}\")" end def to_s(io) @@ -51,8 +59,12 @@ module Shards def initialize(@tag : String) end - def to_hg_ref(simple = false) - simple ? @tag : "tag(\"#{@tag}\")" + def to_hg_ref + @tag + end + + def to_hg_revset + "tag(\"#{@tag}\")" end def to_s(io) @@ -75,7 +87,11 @@ module Shards commit.starts_with?(other.commit) || other.commit.starts_with?(commit) end - def to_hg_ref(simple = false) + def to_hg_ref + @commit + end + + def to_hg_revset @commit end @@ -94,7 +110,11 @@ module Shards end struct HgCurrentRef < HgRef - def to_hg_ref(simple = false) + def to_hg_revset + "." + end + + def to_hg_ref "." end @@ -142,7 +162,7 @@ module Shards ref = hg_ref(version) if file_exists?(ref, SPEC_FILENAME) - capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + capture("hg cat -r #{Process.quote(ref.to_hg_revset)} #{Process.quote(SPEC_FILENAME)}") else Log.debug { "Missing \"#{SPEC_FILENAME}\" for #{name.inspect} at #{ref}" } nil @@ -153,7 +173,7 @@ module Shards update_local_cache begin if file_exists?(ref, SPEC_FILENAME) - spec_yaml = capture("hg cat -r #{Process.quote(ref.to_hg_ref)} #{Process.quote(SPEC_FILENAME)}") + spec_yaml = capture("hg cat -r #{Process.quote(ref.to_hg_revset)} #{Process.quote(SPEC_FILENAME)}") Spec.from_yaml(spec_yaml) end rescue Error @@ -226,11 +246,11 @@ module Shards FileUtils.rm_r(install_path) if File.exists?(install_path) Dir.mkdir_p(install_path) - run "hg clone --quiet -u #{Process.quote(ref.to_hg_ref(true))} -- #{Process.quote(local_path)} #{Process.quote(install_path)}" + run "hg clone --quiet -u #{Process.quote(ref.to_hg_ref)} -- #{Process.quote(local_path)} #{Process.quote(install_path)}" end def commit_sha1_at(ref : HgRef) - capture("hg log -r #{Process.quote(ref.to_hg_ref)} --template #{Process.quote("{node}\n")}").strip + capture("hg log -r #{Process.quote(ref.to_hg_revset)} --template #{Process.quote("{node}\n")}").strip end def local_path @@ -412,7 +432,7 @@ module Shards end private def file_exists?(ref : HgRef, path) - files = capture("hg files -r #{Process.quote(ref.to_hg_ref)} -- #{Process.quote(path)}") + files = capture("hg files -r #{Process.quote(ref.to_hg_revset)} -- #{Process.quote(path)}") !files.strip.empty? end From 1453032e815c566a94d8568e267dd5b0efffac8a Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Tue, 5 Jan 2021 21:27:23 +0100 Subject: [PATCH 20/22] Remove unused method `HgResolver#matches?` --- src/resolvers/hg.cr | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index bfa829ee..4528307d 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -227,19 +227,6 @@ module Shards .compact_map { |tag| Version.new($1) if tag =~ VERSION_TAG } end - def matches?(commit) - if branch = dependency["branch"]? - rev = "branch(\"#{tag}\") and descendants(#{commit})" - elsif bookmark = dependency["bookmark"]? - rev = "bookmark(\"#{bookmark}\") and descendants(#{commit})" - elsif tag = dependency["tag"]? - rev = "tag(\"tag\") and descendants(#{commit})" - else - rev = commit - end - !capture("hg log -r #{Process.quote(rev)}").strip.empty? - end - def install_sources(version : Version, install_path : String) update_local_cache ref = hg_ref(version) From fff4ccc531c8a26eadfcee36e4700cb890ca9c5a Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Tue, 5 Jan 2021 23:22:37 +0100 Subject: [PATCH 21/22] HgResolver#file_exists? does not use exceptions if the file does not exist Exceptions should only be used on real errors, not on expected fails. --- src/resolvers/hg.cr | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/resolvers/hg.cr b/src/resolvers/hg.cr index 4528307d..8887db80 100644 --- a/src/resolvers/hg.cr +++ b/src/resolvers/hg.cr @@ -419,25 +419,24 @@ module Shards end private def file_exists?(ref : HgRef, path) - files = capture("hg files -r #{Process.quote(ref.to_hg_revset)} -- #{Process.quote(path)}") - !files.strip.empty? + 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).not_nil! + run(command, capture: true, path: path).as(String) end - private def run(command, path = local_path, capture = false) + 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) + run_in_current_folder(command, capture, raise_on_fail: raise_on_fail) end end - private def run_in_current_folder(command, capture = false) + private def run_in_current_folder(command, capture = false, raise_on_fail = true) unless HgResolver.has_hg_command? raise Error.new("Error missing hg command line tool. Please install Mercurial first!") end @@ -449,8 +448,12 @@ module Shards status = Process.run(command, shell: true, output: output, error: error) if status.success? - output.to_s if capture - else + 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] From 2e31e09d4f05bb62419aeefc1718100bafafd8c5 Mon Sep 17 00:00:00 2001 From: Frank Fischer Date: Wed, 6 Jan 2021 15:40:14 +0100 Subject: [PATCH 22/22] Add documentation for hg to SPEC.md --- SPEC.md | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index a39de50c..1ac9b0fe 100644 --- a/SPEC.md +++ b/SPEC.md @@ -244,17 +244,36 @@ installations, you must use the generic `git` resolver. Example: `gitlab: thelonlyghost/minitest.cr` +#### hg + +A Mercurial repository URL (String). + +The URL may be any protocol supported by Mercurial, which includes SSH and HTTPS. + +The Merurial repository will be cloned, the list of versions (and associated +`shard.yml`) will be extracted from Mercurial tags (e.g., `v1.2.3`). + +One of the other attributes (`version`, `tag`, `branch`, `bookmark` or `commit`) is +required. When missing, Shards will install the `@` bookmark or `tip`. + +Example: `hg: https://hg.example.org/crystal-library` + + #### commit -Install a Git dependency at the specified commit (String). +Install a Git or Mercurial dependency at the specified commit (String). #### tag -Install a Git dependency at the specified tag (String). +Install a Git or Mercurial dependency at the specified tag (String). #### branch -Install a Git dependency at the specified branch (String). +Install a Git dependency at the specified branch or a Mercurial dependency at the specified named branch (String). + +#### bookmark + +Install a Mercurial dependency at the specified bookmark (String). ### development_dependencies