Skip to content

Commit 02055f9

Browse files
Resolver for Mercurial repositories (#458)
Co-authored-by: Johannes Müller <[email protected]>
1 parent 8f48d4e commit 02055f9

File tree

8 files changed

+785
-5
lines changed

8 files changed

+785
-5
lines changed

.circleci/config.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ jobs:
5151
docker:
5252
- image: crystallang/crystal:latest
5353
steps:
54+
- run:
55+
name: Install mercurial
56+
command: apt-get update && apt-get install mercurial -y
5457
- shards-make-test
5558

5659
test-on-osx:
@@ -60,14 +63,17 @@ jobs:
6063
- with-brew-cache:
6164
steps:
6265
- run:
63-
name: Install Crystal
64-
command: brew install crystal
66+
name: Install Crystal and Mercurial
67+
command: brew install crystal mercurial
6568
- shards-make-test
6669

6770
test-on-nightly:
6871
docker:
6972
- image: crystallang/crystal:nightly
7073
steps:
74+
- run:
75+
name: Install mercurial
76+
command: apt-get update && apt-get install mercurial -y
7177
- shards-make-test
7278

7379
workflows:

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ jobs:
2626
git config --global column.ui always
2727
git config --global core.autocrlf false
2828
29+
- name: Install Python
30+
uses: actions/setup-python@v2
31+
32+
- name: Upgrade pip
33+
run: python -m pip install --upgrade pip
34+
35+
- name: Install Mercurial
36+
run: pip install mercurial
37+
2938
- name: Install Crystal
3039
uses: oprypin/install-crystal@v1
3140
with:

docs/shard.yml.adoc

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,21 @@ Extends the _git_ resolver, and acts exactly like it.
320320
+
321321
*Example:* _bitbucket: tom/library_
322322

323+
*hg*::
324+
325+
A Mercurial repository URL (string).
326+
+
327+
The URL may be [any protocol](https://www.mercurial-scm.org/repo/hg/help/clone)
328+
supported by Mercurial, which includes SSH and HTTPS.
329+
+
330+
The Merurial repository will be cloned, the list of versions (and associated
331+
_shard.yml_) will be extracted from Mercurial tags (e.g., _v1.2.3_).
332+
+
333+
One of the other attributes (_version_, _tag_, _branch_, _bookmark_ or _commit_) is
334+
required. When missing, Shards will install the _@_ bookmark or _tip_.
335+
+
336+
*Example:* _hg: https://hg.example.org/crystal-library_
337+
323338
*version*::
324339
A version requirement (string).
325340
+
@@ -342,13 +357,17 @@ the _~>_ operator has a special meaning, best shown by example:
342357
--
343358

344359
*branch*::
345-
Install the specified branch of a git dependency (string).
360+
Install the specified branch of a git dependency or the named branch
361+
of a mercurial dependency (string).
346362

347363
*commit*::
348-
Install the specified commit of a git dependency (string).
364+
Install the specified commit of a git or mercurial dependency (string).
349365

350366
*tag*::
351-
Install the specified tag of a git dependency (string).
367+
Install the specified tag of a git or mercurial dependency (string).
368+
369+
*bookmark*::
370+
Install the specified bookmark of a mercurial dependency (string).
352371

353372
== Example:
354373

spec/support/factories.cr

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,79 @@ def checkout_git_branch(project, branch)
8383
end
8484
end
8585

86+
def create_hg_repository(project, *versions)
87+
Dir.cd(tmp_path) do
88+
run "hg init #{Process.quote(project)}"
89+
end
90+
91+
Dir.mkdir(File.join(hg_path(project), "src"))
92+
File.write(File.join(hg_path(project), "src", "#{project}.cr"), "module #{project.capitalize}\nend")
93+
94+
Dir.cd(hg_path(project)) do
95+
run "hg add #{Process.quote("src/#{project}.cr")}"
96+
end
97+
98+
versions.each { |version| create_hg_release project, version }
99+
end
100+
101+
def create_fork_hg_repository(project, upstream)
102+
Dir.cd(tmp_path) do
103+
run "hg clone #{Process.quote(hg_url(upstream))} #{Process.quote(project)}"
104+
end
105+
end
106+
107+
def create_hg_version_commit(project, version, shard : Bool | NamedTuple = true)
108+
Dir.cd(hg_path(project)) do
109+
if shard
110+
contents = shard.is_a?(NamedTuple) ? shard : nil
111+
create_shard project, version, contents
112+
end
113+
Dir.cd(hg_path(project)) do
114+
name = shard[:name]? if shard.is_a?(NamedTuple)
115+
name ||= project
116+
File.touch "src/#{name}.cr"
117+
run "hg add #{Process.quote("src/#{name}.cr")}"
118+
end
119+
create_hg_commit project, "release: v#{version}"
120+
end
121+
end
122+
123+
def create_hg_release(project, version, shard : Bool | NamedTuple = true)
124+
create_hg_version_commit(project, version, shard)
125+
create_hg_tag(project, "v#{version}")
126+
end
127+
128+
def create_hg_tag(project, version)
129+
Dir.cd(hg_path(project)) do
130+
run "hg tag -u #{Process.quote("Your Name <[email protected]>")} #{Process.quote(version)}"
131+
end
132+
end
133+
134+
def create_hg_commit(project, message = "new commit")
135+
Dir.cd(hg_path(project)) do
136+
File.write("src/#{project}.cr", "# #{message}", mode: "a")
137+
run "hg commit -u #{Process.quote("Your Name <[email protected]>")} -A -m #{Process.quote(message)}"
138+
end
139+
end
140+
141+
def checkout_new_hg_bookmark(project, branch)
142+
Dir.cd(hg_path(project)) do
143+
run "hg bookmark #{Process.quote(branch)}"
144+
end
145+
end
146+
147+
def checkout_new_hg_branch(project, branch)
148+
Dir.cd(hg_path(project)) do
149+
run "hg branch #{Process.quote(branch)}"
150+
end
151+
end
152+
153+
def checkout_hg_rev(project, rev)
154+
Dir.cd(hg_path(project)) do
155+
run "hg update -C #{Process.quote(rev)}"
156+
end
157+
end
158+
86159
def create_shard(project, version, contents : NamedTuple? = nil)
87160
spec = {name: project, version: version, crystal: Shards.crystal_version}
88161
spec = spec.merge(contents) if contents
@@ -119,6 +192,20 @@ def git_path(project)
119192
File.join(tmp_path, project.to_s)
120193
end
121194

195+
def hg_commits(project, rev = ".")
196+
Dir.cd(hg_path(project)) do
197+
run("hg log --template=#{Process.quote("{node}\n")} -r #{Process.quote(rev)}").strip.split('\n')
198+
end
199+
end
200+
201+
def hg_url(project)
202+
"file://#{Path[hg_path(project)].to_posix}"
203+
end
204+
205+
def hg_path(project)
206+
File.join(tmp_path, project.to_s)
207+
end
208+
122209
def rel_path(project)
123210
"../../spec/.repositories/#{project}"
124211
end

spec/support/requirement.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ def commit(sha1)
66
Shards::GitCommitRef.new(sha1)
77
end
88

9+
def hg_bookmark(name)
10+
Shards::HgBookmarkRef.new(name)
11+
end
12+
13+
def hg_branch(name)
14+
Shards::HgBranchRef.new(name)
15+
end
16+
917
def version(version)
1018
Shards::Version.new(version)
1119
end

spec/unit/hg_resolver_spec.cr

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
require "./spec_helper"
2+
3+
private def resolver(name)
4+
Shards::HgResolver.new(name, hg_url(name))
5+
end
6+
7+
module Shards
8+
# Allow overriding `source` for the specs
9+
class HgResolver
10+
def source=(@source)
11+
end
12+
end
13+
14+
describe HgResolver do
15+
before_each do
16+
create_hg_repository "empty"
17+
create_hg_commit "empty", "initial release"
18+
19+
create_hg_repository "unreleased"
20+
create_hg_version_commit "unreleased", "0.1.0"
21+
checkout_new_hg_branch "unreleased", "branch"
22+
create_hg_commit "unreleased", "testing"
23+
checkout_hg_rev "unreleased", "default"
24+
25+
create_hg_repository "unreleased-bm"
26+
create_hg_version_commit "unreleased-bm", "0.1.0"
27+
checkout_new_hg_bookmark "unreleased-bm", "branch"
28+
create_hg_commit "unreleased-bm", "testing"
29+
checkout_hg_rev "unreleased-bm", "default"
30+
31+
create_hg_repository "library", "0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"
32+
33+
# Create a version tag not prefixed by 'v' which should be ignored
34+
create_hg_tag "library", "99.9.9"
35+
end
36+
37+
it "normalizes github bitbucket gitlab sources" do
38+
# don't normalise other domains
39+
HgResolver.normalize_key_source("hg", "HTTPs://myhgserver.com/Repo").should eq({"hg", "HTTPs://myhgserver.com/Repo"})
40+
41+
# don't change protocol from ssh
42+
HgResolver.normalize_key_source("hg", "ssh://[email protected]/Repo").should eq({"hg", "ssh://[email protected]/Repo"})
43+
end
44+
45+
it "available releases" do
46+
resolver("empty").available_releases.should be_empty
47+
resolver("library").available_releases.should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"])
48+
end
49+
50+
it "latest version for ref" do
51+
expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do
52+
resolver("empty").latest_version_for_ref(hg_branch "default")
53+
end
54+
expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do
55+
resolver("empty").latest_version_for_ref(nil)
56+
end
57+
resolver("unreleased").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}")
58+
resolver("unreleased").latest_version_for_ref(hg_branch "branch").should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased, "branch")[0]}")
59+
resolver("unreleased").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}")
60+
resolver("unreleased-bm").latest_version_for_ref(hg_branch "default").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}")
61+
resolver("unreleased-bm").latest_version_for_ref(hg_bookmark "branch").should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm", "branch")[0]}")
62+
resolver("unreleased-bm").latest_version_for_ref(nil).should eq(version "0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}")
63+
resolver("library").latest_version_for_ref(hg_branch "default").should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}")
64+
resolver("library").latest_version_for_ref(nil).should eq(version "0.2.0+hg.commit.#{hg_commits(:library)[0]}")
65+
expect_raises(Shards::Error, "Could not find branch foo for shard \"library\" in the repository #{hg_url(:library)}") do
66+
resolver("library").latest_version_for_ref(hg_branch "foo")
67+
end
68+
end
69+
70+
it "versions for" do
71+
expect_raises(Shards::Error, "No shard.yml was found for shard \"empty\" at commit #{hg_commits(:empty)[0]}") do
72+
resolver("empty").versions_for(Any)
73+
end
74+
resolver("library").versions_for(Any).should eq(versions ["0.0.1", "0.1.0", "0.1.1", "0.1.2", "0.2.0"])
75+
resolver("library").versions_for(VersionReq.new "~> 0.1.0").should eq(versions ["0.1.0", "0.1.1", "0.1.2"])
76+
resolver("library").versions_for(hg_branch "default").should eq(versions ["0.2.0+hg.commit.#{hg_commits(:library)[0]}"])
77+
resolver("unreleased").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"])
78+
resolver("unreleased").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits(:unreleased)[0]}"])
79+
resolver("unreleased-bm").versions_for(hg_branch "default").should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"])
80+
resolver("unreleased-bm").versions_for(Any).should eq(versions ["0.1.0+hg.commit.#{hg_commits("unreleased-bm")[0]}"])
81+
end
82+
83+
it "read spec for release" do
84+
spec = resolver("library").spec(version "0.1.1")
85+
spec.original_version.should eq(version "0.1.1")
86+
spec.version.should eq(version "0.1.1")
87+
end
88+
89+
it "read spec for commit" do
90+
version = version("0.2.0+hg.commit.#{hg_commits(:library)[0]}")
91+
spec = resolver("library").spec(version)
92+
spec.original_version.should eq(version "0.2.0")
93+
spec.version.should eq(version)
94+
end
95+
96+
it "install" do
97+
library = resolver("library")
98+
99+
library.install_sources(version("0.1.2"), install_path("library"))
100+
File.exists?(install_path("library", "src/library.cr")).should be_true
101+
File.exists?(install_path("library", "shard.yml")).should be_true
102+
Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.1.2")
103+
104+
library.install_sources(version("0.2.0"), install_path("library"))
105+
Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0")
106+
end
107+
108+
it "install commit" do
109+
library = resolver("library")
110+
version = version "0.2.0+hg.commit.#{hg_commits(:library)[0]}"
111+
library.install_sources(version, install_path("library"))
112+
Spec.from_file(install_path("library", "shard.yml")).version.should eq(version "0.2.0")
113+
end
114+
115+
it "origin changed" do
116+
library = HgResolver.new("library", hg_url("library"))
117+
library.install_sources(version("0.1.2"), install_path("library"))
118+
119+
# Change the origin in the cache repo to https://foss.heptapod.net/foo/bar
120+
hgrc_path = File.join(library.local_path, ".hg", "hgrc")
121+
hgrc = File.read(hgrc_path)
122+
hgrc = hgrc.gsub(/(default\s*=\s*)([^\r\n]*)/, "\\1https://foss.heptapod.net/foo/bar")
123+
File.write(hgrc_path, hgrc)
124+
#
125+
# All of these alternatives should not trigger origin as changed
126+
same_origins = [
127+
"https://foss.heptapod.net/foo/bar",
128+
"https://foss.heptapod.net:1234/foo/bar",
129+
"http://foss.heptapod.net/foo/bar",
130+
"ssh://foss.heptapod.net/foo/bar",
131+
"hg://foss.heptapod.net/foo/bar",
132+
"rsync://foss.heptapod.net/foo/bar",
133+
"[email protected]:foo/bar",
134+
"[email protected]:foo/bar",
135+
"foss.heptapod.net:foo/bar",
136+
]
137+
138+
same_origins.each do |origin|
139+
library.source = origin
140+
library.origin_changed?.should be_false
141+
end
142+
143+
# These alternatives should all trigger origin as changed
144+
changed_origins = [
145+
"https://foss.heptapod.net/foo/bar2",
146+
"https://foss.heptapod.net/foos/bar",
147+
"https://hghubz.com/foo/bar",
148+
"file:///foss.heptapod.net/foo/bar",
149+
"[email protected]:foo/bar2",
150+
"[email protected]:foo/bar",
151+
"",
152+
]
153+
154+
changed_origins.each do |origin|
155+
library.source = origin
156+
library.origin_changed?.should be_true
157+
end
158+
end
159+
160+
it "renders report version" do
161+
resolver("library").report_version(version "1.2.3").should eq("1.2.3")
162+
resolver("library").report_version(version "1.2.3+hg.commit.654875c9dbfa8d72fba70d65fd548d51ffb85aff").should eq("1.2.3 at 654875c")
163+
end
164+
165+
it "#matches_ref" do
166+
resolver = HgResolver.new("", "")
167+
resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567")).should be_true
168+
resolver.matches_ref?(HgCommitRef.new("1234567890abcdef"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true
169+
resolver.matches_ref?(HgCommitRef.new("1234567"), Shards::Version.new("0.1.0.+hg.commit.1234567890abcdef")).should be_true
170+
end
171+
end
172+
end

src/config.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Shards
1212
VERSION_REFERENCE = /^v?\d+[-.][-.a-zA-Z\d]+$/
1313
VERSION_TAG = /^v(\d+[-.][-.a-zA-Z\d]+)$/
1414
VERSION_AT_GIT_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+git\.commit\.([0-9a-f]+)$/
15+
VERSION_AT_HG_COMMIT = /^(\d+[-.][-.a-zA-Z\d]+)\+hg\.commit\.([0-9a-f]+)$/
1516

1617
def self.cache_path
1718
@@cache_path ||= find_or_create_cache_path

0 commit comments

Comments
 (0)