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

Resolver for Mercurial repositories #458

Merged
merged 20 commits into from Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 8 additions & 2 deletions .circleci/config.yml
Expand Up @@ -51,6 +51,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:
Expand All @@ -60,14 +63,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:
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -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:
Expand Down
25 changes: 22 additions & 3 deletions docs/shard.yml.adoc
Expand Up @@ -320,6 +320,21 @@ Extends the _git_ resolver, and acts exactly like it.
+
*Example:* _bitbucket: tom/library_

*hg*::

A Mercurial repository URL (string).
+
The URL may be [any protocol](https://www.mercurial-scm.org/repo/hg/help/clone)
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_

*version*::
A version requirement (string).
+
Expand All @@ -342,13 +357,17 @@ the _~>_ operator has a special meaning, best shown by example:
--

*branch*::
Install the specified branch of a git dependency (string).
Install the specified branch of a git dependency or the named branch
of a mercurial dependency (string).

*commit*::
Install the specified commit of a git dependency (string).
Install the specified commit of a git or mercurial dependency (string).

*tag*::
Install the specified tag of a git dependency (string).
Install the specified tag of a git or mercurial dependency (string).

*bookmark*::
Install the specified bookmark of a mercurial dependency (string).

== Example:

Expand Down
87 changes: 87 additions & 0 deletions spec/support/factories.cr
Expand Up @@ -83,6 +83,79 @@ 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 -u #{Process.quote("Your Name <you@example.com>")} #{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 -u #{Process.quote("Your Name <you@example.com>")} -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
Expand Down Expand Up @@ -119,6 +192,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=#{Process.quote("{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
Expand Down
8 changes: 8 additions & 0 deletions spec/support/requirement.cr
Expand Up @@ -6,6 +6,14 @@ 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

def version(version)
Shards::Version.new(version)
end
Expand Down
172 changes: 172 additions & 0 deletions spec/unit/hg_resolver_spec.cr
@@ -0,0 +1,172 @@
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 "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
create_hg_tag "library", "99.9.9"
end

it "normalizes github bitbucket gitlab sources" do
beta-ziliani marked this conversation as resolved.
Show resolved Hide resolved
# don't normalise other domains
HgResolver.normalize_key_source("hg", "HTTPs://myhgserver.com/Repo").should eq({"hg", "HTTPs://myhgserver.com/Repo"})

# don't change protocol from ssh
HgResolver.normalize_key_source("hg", "ssh://hg@myhgserver.com/Repo").should eq({"hg", "ssh://hg@myhgserver.com/Repo"})
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("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
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]}"])
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
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
1 change: 1 addition & 0 deletions src/config.cr
Expand Up @@ -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
Expand Down