1 /**
2 	Dependency specification functionality.
3 
4 	Copyright: © 2012-2013 Matthias Dondorff, © 2012-2016 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff, Sönke Ludwig
7 */
8 module dub.dependency;
9 
10 import dub.internal.utils;
11 import dub.internal.vibecompat.core.file;
12 import dub.internal.vibecompat.data.json;
13 import dub.internal.vibecompat.inet.path;
14 import dub.package_;
15 import dub.semver;
16 import dub.internal.logging;
17 
18 import dub.internal.dyaml.stdsumtype;
19 
20 import std.algorithm;
21 import std.array;
22 import std.exception;
23 import std.string;
24 
25 
26 /** Encapsulates the name of a package along with its dependency specification.
27 */
28 struct PackageDependency {
29 	/// Name of the referenced package.
30 	string name;
31 
32 	/// Dependency specification used to select a particular version of the package.
33 	Dependency spec;
34 }
35 
36 /**
37 	Represents a dependency specification.
38 
39 	A dependency specification either represents a specific version or version
40 	range, or a path to a package. In addition to that it has `optional` and
41 	`default_` flags to control how non-mandatory dependencies are handled. The
42 	package name is notably not part of the dependency specification.
43 */
44 struct Dependency {
45 	/// We currently support 3 'types'
46 	private alias Value = SumType!(VersionRange, NativePath, Repository);
47 
48 	/// Used by `toString`
49 	private static immutable string[] BooleanOptions = [ "optional", "default" ];
50 
51 	// Shortcut to create >=0.0.0
52 	private enum ANY_IDENT = "*";
53 
54 	private Value m_value;
55 	private bool m_optional;
56 	private bool m_default;
57 
58 	/// A Dependency, which matches every valid version.
59 	static @property Dependency any() @safe { return Dependency(VersionRange.Any); }
60 
61 	/// An invalid dependency (with no possible version matches).
62 	static @property Dependency invalid() @safe
63 	{
64 		return Dependency(VersionRange.Invalid);
65 	}
66 
67 	/** Constructs a new dependency specification that matches a specific
68 		path.
69 	*/
70 	this(NativePath path) @safe
71 	{
72 		this.m_value = path;
73 	}
74 
75 	/** Constructs a new dependency specification that matches a specific
76 		Git reference.
77 	*/
78 	this(Repository repository) @safe
79 	{
80 		this.m_value = repository;
81 	}
82 
83 	/** Constructs a new dependency specification from a string
84 
85 		See the `versionSpec` property for a description of the accepted
86 		contents of that string.
87 	*/
88 	this(string spec) @safe
89 	{
90 		this(VersionRange.fromString(spec));
91 	}
92 
93 	/** Constructs a new dependency specification that matches a specific
94 		version.
95 	*/
96 	this(const Version ver) @safe
97 	{
98 		this(VersionRange(ver, ver));
99 	}
100 
101 	/// Construct a version from a range of possible values
102 	this (VersionRange rng) @safe
103 	{
104 		this.m_value = rng;
105 	}
106 
107 	deprecated("Instantiate the `Repository` struct with the string directly")
108 	this(Repository repository, string spec) @safe
109 	{
110 		assert(repository.m_ref is null);
111 		repository.m_ref = spec;
112 		this(repository);
113 	}
114 
115 	/// If set, overrides any version based dependency selection.
116 	deprecated("Construct a new `Dependency` object instead")
117 	@property void path(NativePath value) @trusted
118 	{
119 		this.m_value = value;
120 	}
121 	/// ditto
122 	@property NativePath path() const @safe
123 	{
124 		return this.m_value.match!(
125 			(const NativePath p) => p,
126 			(      any         ) => NativePath.init,
127 		);
128 	}
129 
130 	/// If set, overrides any version based dependency selection.
131 	deprecated("Construct a new `Dependency` object instead")
132 	@property void repository(Repository value) @trusted
133 	{
134 		this.m_value = value;
135 	}
136 	/// ditto
137 	@property Repository repository() const @safe
138 	{
139 		return this.m_value.match!(
140 			(const Repository p) => p,
141 			(      any         ) => Repository.init,
142 		);
143 	}
144 
145 	/// Determines if the dependency is required or optional.
146 	@property bool optional() const scope @safe pure nothrow @nogc
147 	{
148 		return m_optional;
149 	}
150 	/// ditto
151 	@property void optional(bool optional) scope @safe pure nothrow @nogc
152 	{
153 		m_optional = optional;
154 	}
155 
156 	/// Determines if an optional dependency should be chosen by default.
157 	@property bool default_() const scope @safe pure nothrow @nogc
158 	{
159 		return m_default;
160 	}
161 	/// ditto
162 	@property void default_(bool value) scope @safe pure nothrow @nogc
163 	{
164 		m_default = value;
165 	}
166 
167 	/// Returns true $(I iff) the version range only matches a specific version.
168 	@property bool isExactVersion() const scope @safe
169 	{
170 		return this.m_value.match!(
171 			(NativePath v) => false,
172 			(Repository v) => false,
173 			(VersionRange v) => v.isExactVersion(),
174 		);
175 	}
176 
177 	/// Returns the exact version matched by the version range.
178 	@property Version version_() const @safe {
179 		auto range = this.m_value.match!(
180 			// Can be simplified to `=> assert(0)` once we drop support for v2.096
181 			(NativePath   p) { int dummy; if (dummy) return VersionRange.init; assert(0); },
182 			(Repository   r) { int dummy; if (dummy) return VersionRange.init; assert(0); },
183 			(VersionRange v) => v,
184 		);
185 		enforce(range.isExactVersion(),
186 				"Dependency "~range.toString()~" is no exact version.");
187 		return range.m_versA;
188 	}
189 
190 	/// Sets/gets the matching version range as a specification string.
191 	deprecated("Create a new `Dependency` instead and provide a `VersionRange`")
192 	@property void versionSpec(string ves) @trusted
193 	{
194 		this.m_value = VersionRange.fromString(ves);
195 	}
196 
197 	/// ditto
198 	deprecated("Use `Dependency.visit` and match `VersionRange`instead")
199 	@property string versionSpec() const @safe {
200 		return this.m_value.match!(
201 			(const NativePath   p) => ANY_IDENT,
202 			(const Repository   r) => r.m_ref,
203 			(const VersionRange p) => p.toString(),
204 		);
205 	}
206 
207 	/** Returns a modified dependency that gets mapped to a given path.
208 
209 		This function will return an unmodified `Dependency` if it is not path
210 		based. Otherwise, the given `path` will be prefixed to the existing
211 		path.
212 	*/
213 	Dependency mapToPath(NativePath path) const @trusted {
214 		// NOTE Path is @system in vibe.d 0.7.x and in the compatibility layer
215 		return this.m_value.match!(
216 			(NativePath v) {
217 				if (v.empty || v.absolute) return this;
218 				auto ret = Dependency(path ~ v);
219 				ret.m_default = m_default;
220 				ret.m_optional = m_optional;
221 				return ret;
222 			},
223 			(Repository v) => this,
224 			(VersionRange v) => this,
225 		);
226 	}
227 
228 	/** Returns a human-readable string representation of the dependency
229 		specification.
230 	*/
231 	string toString() const scope @trusted {
232 		// Trusted because `SumType.match` doesn't seem to support `scope`
233 
234 		string Stringifier (T, string pre = null) (const T v)
235 		{
236 			const bool extra = this.optional || this.default_;
237 			return format("%s%s%s%-(%s, %)%s",
238 					pre, v,
239 					extra ? " (" : "",
240 					BooleanOptions[!this.optional .. 1 + this.default_],
241 					extra ? ")" : "");
242 		}
243 
244 		return this.m_value.match!(
245 			Stringifier!Repository,
246 			Stringifier!(NativePath, "@"),
247 			Stringifier!VersionRange
248 		);
249 	}
250 
251 	/** Returns a JSON representation of the dependency specification.
252 
253 		Simple specifications will be represented as a single specification
254 		string (`versionSpec`), while more complex specifications will be
255 		represented as a JSON object with optional "version", "path", "optional"
256 		and "default" fields.
257 
258 		Params:
259 		  selections = We are serializing `dub.selections.json`, don't write out
260 			  `optional` and `default`.
261 	*/
262 	Json toJson(bool selections = false) const @safe
263 	{
264 		// NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer
265 		static void initJson(ref Json j, bool opt, bool def, bool s = selections)
266 		{
267 			j = Json.emptyObject;
268 			if (!s && opt) j["optional"] = true;
269 			if (!s && def) j["default"] = true;
270 		}
271 
272 		Json json;
273 		this.m_value.match!(
274 			(const NativePath v) @trusted {
275 				initJson(json, optional, default_);
276 				json["path"] = v.toString();
277 			},
278 
279 			(const Repository v) @trusted {
280 				initJson(json, optional, default_);
281 				json["repository"] = v.toString();
282 				json["version"] = v.m_ref;
283 			},
284 
285 			(const VersionRange v) @trusted {
286 				if (!selections && (optional || default_))
287 				{
288 					initJson(json, optional, default_);
289 					json["version"] = v.toString();
290 				}
291 				else
292 					json = Json(v.toString());
293 			},
294 		);
295 		return json;
296 	}
297 
298 	@trusted unittest {
299 		Dependency d = Dependency("==1.0.0");
300 		assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString());
301 		d = fromJson((fromJson(d.toJson())).toJson());
302 		assert(d == Dependency("1.0.0"));
303 		assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString());
304 	}
305 
306 	@trusted unittest {
307 		Dependency dependency = Dependency(Repository("git+http://localhost", "1.0.0"));
308 		Json expected = Json([
309 			"repository": Json("git+http://localhost"),
310 			"version": Json("1.0.0")
311 		]);
312 		assert(dependency.toJson() == expected, "Failed: " ~ dependency.toJson().toPrettyString());
313 	}
314 
315 	@trusted unittest {
316 		Dependency d = Dependency(NativePath("dir"));
317 		Json expected = Json([ "path": Json("dir") ]);
318 		assert(d.toJson() == expected, "Failed: " ~ d.toJson().toPrettyString());
319 	}
320 
321 	/** Constructs a new `Dependency` from its JSON representation.
322 
323 		See `toJson` for a description of the JSON format.
324 	*/
325 	static Dependency fromJson(Json verspec)
326 	@trusted { // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer
327 		Dependency dep;
328 		if( verspec.type == Json.Type.object ){
329 			if( auto pp = "path" in verspec ) {
330 				if (auto pv = "version" in verspec)
331 					logDiagnostic("Ignoring version specification (%s) for path based dependency %s", pv.get!string, pp.get!string);
332 
333 				dep = Dependency(NativePath(verspec["path"].get!string));
334 			} else if (auto repository = "repository" in verspec) {
335 				enforce("version" in verspec, "No version field specified!");
336 				enforce(repository.length > 0, "No repository field specified!");
337 
338 				dep = Dependency(Repository(
339                                      repository.get!string, verspec["version"].get!string));
340 			} else {
341 				enforce("version" in verspec, "No version field specified!");
342 				auto ver = verspec["version"].get!string;
343 				// Using the string to be able to specify a range of versions.
344 				dep = Dependency(ver);
345 			}
346 
347 			if (auto po = "optional" in verspec) dep.optional = po.get!bool;
348 			if (auto po = "default" in verspec) dep.default_ = po.get!bool;
349 		} else {
350 			// canonical "package-id": "version"
351 			dep = Dependency(verspec.get!string);
352 		}
353 		return dep;
354 	}
355 
356 	@trusted unittest {
357 		assert(fromJson(parseJsonString("\">=1.0.0 <2.0.0\"")) == Dependency(">=1.0.0 <2.0.0"));
358 		Dependency parsed = fromJson(parseJsonString(`
359 		{
360 			"version": "2.0.0",
361 			"optional": true,
362 			"default": true,
363 			"path": "path/to/package"
364 		}
365 			`));
366 		Dependency d = NativePath("path/to/package"); // supposed to ignore the version spec
367 		d.optional = true;
368 		d.default_ = true;
369 		assert(d == parsed);
370 	}
371 
372 	/** Compares dependency specifications.
373 
374 		These methods are suitable for equality comparisons, as well as for
375 		using `Dependency` as a key in hash or tree maps.
376 	*/
377 	bool opEquals(in Dependency o) const scope @safe {
378 		if (o.m_optional != this.m_optional) return false;
379 		if (o.m_default  != this.m_default)  return false;
380 		return this.m_value == o.m_value;
381 	}
382 
383 	/// ditto
384 	int opCmp(in Dependency o) const @safe {
385 		alias ResultMatch = match!(
386 			(VersionRange r1, VersionRange r2) => r1.opCmp(r2),
387 			(_1, _2) => 0,
388 		);
389 		if (auto result = ResultMatch(this.m_value, o.m_value))
390 			return result;
391 		if (m_optional != o.m_optional) return m_optional ? -1 : 1;
392 		return 0;
393 	}
394 
395 	/** Determines if this dependency specification is valid.
396 
397 		A specification is valid if it can match at least one version.
398 	*/
399 	bool valid() const @safe {
400 		return this.m_value.match!(
401 			(NativePath v) => true,
402 			(Repository v) => true,
403 			(VersionRange v) => v.isValid(),
404 		);
405 	}
406 
407 	/** Determines if this dependency specification matches arbitrary versions.
408 
409 		This is true in particular for the `any` constant.
410 	*/
411 	deprecated("Use `VersionRange.matchesAny` directly")
412 	bool matchesAny() const scope @safe {
413 		return this.m_value.match!(
414 			(NativePath v) => true,
415 			(Repository v) => true,
416 			(VersionRange v) => v.matchesAny(),
417 		);
418 	}
419 
420 	/** Tests if the specification matches a specific version.
421 	*/
422 	bool matches(string vers, VersionMatchMode mode = VersionMatchMode.standard) const @safe
423 	{
424 		return matches(Version(vers), mode);
425 	}
426 	/// ditto
427 	bool matches(in  Version v, VersionMatchMode mode = VersionMatchMode.standard) const @safe {
428 		return this.m_value.match!(
429 			(NativePath i) => true,
430 			(Repository i) => true,
431 			(VersionRange i) => i.matchesAny() || i.matches(v, mode),
432 		);
433 	}
434 
435 	/** Merges two dependency specifications.
436 
437 		The result is a specification that matches the intersection of the set
438 		of versions matched by the individual specifications. Note that this
439 		result can be invalid (i.e. not match any version).
440 	*/
441 	Dependency merge(ref const(Dependency) o) const @trusted {
442 		alias Merger = match!(
443 			(const NativePath a, const NativePath b) => a == b ? this : invalid,
444 			(const NativePath a,       any         ) => o,
445 			(      any         , const NativePath b) => this,
446 
447 			(const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : invalid,
448 			(const Repository a,       any         ) => this,
449 			(      any         , const Repository b) => o,
450 
451 			(const VersionRange a, const VersionRange b) {
452 				if (a.matchesAny()) return o;
453 				if (b.matchesAny()) return this;
454 
455 				VersionRange copy = a;
456 				copy.merge(b);
457 				if (!copy.isValid()) return invalid;
458 				return Dependency(copy);
459 			}
460 		);
461 
462 		Dependency ret = Merger(this.m_value, o.m_value);
463 		ret.m_optional = m_optional && o.m_optional;
464 		return ret;
465 	}
466 }
467 
468 /// Allow direct access to the underlying dependency
469 public auto visit (Handlers...) (const auto ref Dependency dep)
470 {
471     return dep.m_value.match!(Handlers);
472 }
473 
474 //// Ditto
475 public auto visit (Handlers...) (auto ref Dependency dep)
476 {
477     return dep.m_value.match!(Handlers);
478 }
479 
480 
481 unittest {
482 	Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0");
483 	assert (a.merge(b).valid() && a.merge(b).toString() == ">=1.3.0", a.merge(b).toString());
484 
485 	assertThrown(Dependency("<=2.0.0 >=1.0.0"));
486 	assertThrown(Dependency(">=2.0.0 <=1.0.0"));
487 
488 	a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0");
489 	assert (a.merge(b).valid() && a.merge(b).toString() == ">=2.0.0 <=5.0.0", a.merge(b).toString());
490 
491 	assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid");
492 
493 	a = Dependency(">1.0.0"); b = Dependency("<2.0.0");
494 	assert (a.merge(b).valid(), a.merge(b).toString());
495 	assert (a.merge(b).toString() == ">1.0.0 <2.0.0", a.merge(b).toString());
496 
497 	a = Dependency(">2.0.0"); b = Dependency("<1.0.0");
498 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
499 
500 	a = Dependency(">=2.0.0"); b = Dependency("<=1.0.0");
501 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
502 
503 	a = Dependency("==2.0.0"); b = Dependency("==1.0.0");
504 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
505 
506 	a = Dependency("1.0.0"); b = Dependency("==1.0.0");
507 	assert (a == b);
508 
509 	a = Dependency("<=2.0.0"); b = Dependency("==1.0.0");
510 	Dependency m = a.merge(b);
511 	assert (m.valid(), m.toString());
512 	assert (m.matches(Version("1.0.0")));
513 	assert (!m.matches(Version("1.1.0")));
514 	assert (!m.matches(Version("0.0.1")));
515 
516 
517 	// branches / head revisions
518 	a = Dependency(Version.masterBranch);
519 	assert(a.valid());
520 	assert(a.matches(Version.masterBranch));
521 	b = Dependency(Version.masterBranch);
522 	m = a.merge(b);
523 	assert(m.matches(Version.masterBranch));
524 
525 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid");
526 	assertThrown(a = Dependency(">=1.0.0 " ~ Version.masterBranch.toString()), "Construction invalid");
527 
528 	immutable string branch1 = Version.branchPrefix ~ "Branch1";
529 	immutable string branch2 = Version.branchPrefix ~ "Branch2";
530 
531 	//assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded");
532 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded");
533 
534 	a = Dependency(branch1);
535 	b = Dependency(branch2);
536 	assert(!a.merge(b).valid, "Shouldn't be able to merge to different branches");
537 	b = a.merge(a);
538 	assert(b.valid, "Should be able to merge the same branches. (?)");
539 	assert(a == b);
540 
541 	a = Dependency(branch1);
542 	assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'");
543 	assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')");
544 	assert(!a.matches(Version.masterBranch), "Dependency(branch1) matches Version.masterBranch");
545 	assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'");
546 	assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'");
547 	a = Dependency(">=1.0.0");
548 	assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'");
549 
550 	// Testing optional dependencies.
551 	a = Dependency(">=1.0.0");
552 	assert(!a.optional, "Default is not optional.");
553 	b = a;
554 	assert(!a.merge(b).optional, "Merging two not optional dependencies wrong.");
555 	a.optional = true;
556 	assert(!a.merge(b).optional, "Merging optional with not optional wrong.");
557 	b.optional = true;
558 	assert(a.merge(b).optional, "Merging two optional dependencies wrong.");
559 
560 	// SemVer's sub identifiers.
561 	a = Dependency(">=1.0.0-beta");
562 	assert(!a.matches(Version("1.0.0-alpha")), "Failed: match 1.0.0-alpha with >=1.0.0-beta");
563 	assert(a.matches(Version("1.0.0-beta")), "Failed: match 1.0.0-beta with >=1.0.0-beta");
564 	assert(a.matches(Version("1.0.0")), "Failed: match 1.0.0 with >=1.0.0-beta");
565 	assert(a.matches(Version("1.0.0-rc")), "Failed: match 1.0.0-rc with >=1.0.0-beta");
566 
567 	// Approximate versions.
568 	a = Dependency("~>3.0");
569 	b = Dependency(">=3.0.0 <4.0.0-0");
570 	assert(a == b, "Testing failed: " ~ a.toString());
571 	assert(a.matches(Version("3.1.146")), "Failed: Match 3.1.146 with ~>0.1.2");
572 	assert(!a.matches(Version("0.2.0")), "Failed: Match 0.2.0 with ~>0.1.2");
573 	assert(!a.matches(Version("4.0.0-beta.1")));
574 	a = Dependency("~>3.0.0");
575 	assert(a == Dependency(">=3.0.0 <3.1.0-0"), "Testing failed: " ~ a.toString());
576 	a = Dependency("~>3.5");
577 	assert(a == Dependency(">=3.5.0 <4.0.0-0"), "Testing failed: " ~ a.toString());
578 	a = Dependency("~>3.5.0");
579 	assert(a == Dependency(">=3.5.0 <3.6.0-0"), "Testing failed: " ~ a.toString());
580 	assert(!Dependency("~>3.0.0").matches(Version("3.1.0-beta")));
581 
582 	a = Dependency("^0.1.2");
583 	assert(a == Dependency(">=0.1.2 <0.1.3-0"));
584 	a = Dependency("^1.2.3");
585 	assert(a == Dependency(">=1.2.3 <2.0.0-0"), "Testing failed: " ~ a.toString());
586 	a = Dependency("^1.2");
587 	assert(a == Dependency(">=1.2.0 <2.0.0-0"), "Testing failed: " ~ a.toString());
588 
589 	a = Dependency("~>0.1.1");
590 	b = Dependency("==0.1.0");
591 	assert(!a.merge(b).valid);
592 	b = Dependency("==0.1.9999");
593 	assert(a.merge(b).valid);
594 	b = Dependency("==0.2.0");
595 	assert(!a.merge(b).valid);
596 	b = Dependency("==0.2.0-beta.1");
597 	assert(!a.merge(b).valid);
598 
599 	a = Dependency("~>1.0.1-beta");
600 	b = Dependency(">=1.0.1-beta <1.1.0-0");
601 	assert(a == b, "Testing failed: " ~ a.toString());
602 	assert(a.matches(Version("1.0.1-beta")));
603 	assert(a.matches(Version("1.0.1-beta.6")));
604 
605 	a = Dependency("~d2test");
606 	assert(!a.optional);
607 	assert(a.valid);
608 	assert(a.version_ == Version("~d2test"));
609 
610 	a = Dependency("==~d2test");
611 	assert(!a.optional);
612 	assert(a.valid);
613 	assert(a.version_ == Version("~d2test"));
614 
615 	a = Dependency.any;
616 	assert(!a.optional);
617 	assert(a.valid);
618 	assertThrown(a.version_);
619 	assert(a.matches(Version.masterBranch));
620 	assert(a.matches(Version("1.0.0")));
621 	assert(a.matches(Version("0.0.1-pre")));
622 	b = Dependency(">=1.0.1");
623 	assert(b == a.merge(b));
624 	assert(b == b.merge(a));
625 	b = Dependency(Version.masterBranch);
626 	assert(a.merge(b) == b);
627 	assert(b.merge(a) == b);
628 
629 	a.optional = true;
630 	assert(a.matches(Version.masterBranch));
631 	assert(a.matches(Version("1.0.0")));
632 	assert(a.matches(Version("0.0.1-pre")));
633 	b = Dependency(">=1.0.1");
634 	assert(b == a.merge(b));
635 	assert(b == b.merge(a));
636 	b = Dependency(Version.masterBranch);
637 	assert(a.merge(b) == b);
638 	assert(b.merge(a) == b);
639 
640 	assert(Dependency("1.0.0").matches(Version("1.0.0+foo")));
641 	assert(Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard));
642 	assert(!Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
643 	assert(Dependency("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
644 	assert(Dependency("~>1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
645 	assert(Dependency("~>1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
646 
647 	logDebug("Dependency unittest success.");
648 }
649 
650 unittest {
651 	assert(VersionRange.fromString("~>1.0.4").toString() == "~>1.0.4");
652 	assert(VersionRange.fromString("~>1.4").toString() == "~>1.4");
653 	assert(VersionRange.fromString("~>2").toString() == "~>2");
654 	assert(VersionRange.fromString("~>1.0.4+1.2.3").toString() == "~>1.0.4");
655 	assert(VersionRange.fromString("^0.1.2").toString() == "^0.1.2");
656 	assert(VersionRange.fromString("^1.2.3").toString() == "^1.2.3");
657 	assert(VersionRange.fromString("^1.2").toString() == "~>1.2"); // equivalent; prefer ~>
658 }
659 
660 /**
661 	Represents an SCM repository.
662 */
663 struct Repository
664 {
665 	private string m_remote;
666 	private string m_ref;
667 
668 	private Kind m_kind;
669 
670 	enum Kind
671 	{
672 		git,
673 	}
674 
675 	/**
676 		Params:
677 			remote = Repository remote.
678 			ref_   = Reference to use (SHA1, tag, branch name...)
679 	 */
680 	this(string remote, string ref_)
681 	{
682 		enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
683 
684 		m_remote = remote["git+".length .. $];
685 		m_kind = Kind.git;
686 		m_ref = ref_;
687 		assert(m_remote.length);
688 		assert(m_ref.length);
689 	}
690 
691 	/// Ditto
692 	deprecated("Use the constructor accepting a second parameter named `ref_`")
693 	this(string remote)
694 	{
695 		enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
696 
697 		m_remote = remote["git+".length .. $];
698 		m_kind = Kind.git;
699 		assert(m_remote.length);
700 	}
701 
702 	string toString() const nothrow pure @safe
703 	{
704 		if (empty) return null;
705 		string kindRepresentation;
706 
707 		final switch (kind)
708 		{
709 			case Kind.git:
710 				kindRepresentation = "git";
711 		}
712 		return kindRepresentation~"+"~remote;
713 	}
714 
715 	/**
716 		Returns:
717 			Repository URL or path.
718 	*/
719 	@property string remote() const @nogc nothrow pure @safe
720 	in { assert(m_remote !is null); }
721 	do
722 	{
723 		return m_remote;
724 	}
725 
726 	/**
727 		Returns:
728 			The reference (commit hash, branch name, tag) we are targeting
729 	*/
730 	@property string ref_() const @nogc nothrow pure @safe
731 	in { assert(m_remote !is null); }
732 	in { assert(m_ref !is null); }
733 	do
734 	{
735 		return m_ref;
736 	}
737 
738 	/**
739 		Returns:
740 			Repository type.
741 	*/
742 	@property Kind kind() const @nogc nothrow pure @safe
743 	{
744 		return m_kind;
745 	}
746 
747 	/**
748 		Returns:
749 			Whether the repository was initialized with an URL or path.
750 	*/
751 	@property bool empty() const @nogc nothrow pure @safe
752 	{
753 		return m_remote.empty;
754 	}
755 }
756 
757 
758 /**
759 	Represents a version in semantic version format, or a branch identifier.
760 
761 	This can either have the form "~master", where "master" is a branch name,
762 	or the form "major.update.bugfix-prerelease+buildmetadata" (see the
763 	Semantic Versioning Specification v2.0.0 at http://semver.org/).
764 */
765 struct Version {
766 	private {
767 		static immutable MAX_VERS = "99999.0.0";
768 		static immutable masterString = "~master";
769 		enum branchPrefix = '~';
770 		string m_version;
771 	}
772 
773 	static immutable Version minRelease = Version("0.0.0");
774 	static immutable Version maxRelease = Version(MAX_VERS);
775 	static immutable Version masterBranch = Version(masterString);
776 
777 	/** Constructs a new `Version` from its string representation.
778 	*/
779 	this(string vers) @safe pure
780 	{
781 		enforce(vers.length > 1, "Version strings must not be empty.");
782 		if (vers[0] != branchPrefix)
783 			enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers);
784 		m_version = vers;
785 	}
786 
787 	/** Constructs a new `Version` from its string representation.
788 
789 		This method is equivalent to calling the constructor and is used as an
790 		endpoint for the serialization framework.
791 	*/
792 	static Version fromString(string vers) @safe pure { return Version(vers); }
793 
794 	bool opEquals(in Version oth) const scope @safe pure
795 	{
796 		return opCmp(oth) == 0;
797 	}
798 
799 	/// Tests if this represents a branch instead of a version.
800 	@property bool isBranch() const scope @safe pure nothrow @nogc
801 	{
802 		return m_version.length > 0 && m_version[0] == branchPrefix;
803 	}
804 
805 	/// Tests if this represents the master branch "~master".
806 	@property bool isMaster() const scope @safe pure nothrow @nogc
807 	{
808 		return m_version == masterString;
809 	}
810 
811 	/** Tests if this represents a pre-release version.
812 
813 		Note that branches are always considered pre-release versions.
814 	*/
815 	@property bool isPreRelease() const scope @safe pure nothrow @nogc
816 	{
817 		if (isBranch) return true;
818 		return isPreReleaseVersion(m_version);
819 	}
820 
821 	/** Tests two versions for equality, according to the selected match mode.
822 	*/
823 	bool matches(in Version other, VersionMatchMode mode = VersionMatchMode.standard)
824 	const scope @safe pure
825 	{
826 		if (mode == VersionMatchMode.strict)
827 			return this.toString() == other.toString();
828 		return this == other;
829 	}
830 
831 	/** Compares two versions/branches for precedence.
832 
833 		Versions generally have precedence over branches and the master branch
834 		has precedence over other branches. Apart from that, versions are
835 		compared using SemVer semantics, while branches are compared
836 		lexicographically.
837 	*/
838 	int opCmp(in Version other) const scope @safe pure
839 	{
840 		if (isBranch || other.isBranch) {
841 			if(m_version == other.m_version) return 0;
842 			if (!isBranch) return 1;
843 			else if (!other.isBranch) return -1;
844 			if (isMaster) return 1;
845 			else if (other.isMaster) return -1;
846 			return this.m_version < other.m_version ? -1 : 1;
847 		}
848 
849 		return compareVersions(m_version, other.m_version);
850 	}
851 
852 	/// Returns the string representation of the version/branch.
853 	string toString() const return scope @safe pure nothrow @nogc
854 	{
855 		return m_version;
856 	}
857 }
858 
859 /**
860  * A range of versions that are acceptable
861  *
862  * While not directly described in SemVer v2.0.0, a common set
863  * of range operators have appeared among package managers.
864  * We mostly NPM's: https://semver.npmjs.com/
865  *
866  * Hence the acceptable forms for this string are as follows:
867  *
868  * $(UL
869  *  $(LI `"1.0.0"` - a single version in SemVer format)
870  *  $(LI `"==1.0.0"` - alternative single version notation)
871  *  $(LI `">1.0.0"` - version range with a single bound)
872  *  $(LI `">1.0.0 <2.0.0"` - version range with two bounds)
873  *  $(LI `"~>1.0.0"` - a fuzzy version range)
874  *  $(LI `"~>1.0"` - a fuzzy version range with partial version)
875  *  $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z))
876  *  $(LI `"^1.0"` - same as ^1.0.0)
877  *  $(LI `"~master"` - a branch name)
878  *  $(LI `"*"` - match any version (see also `VersionRange.Any`))
879  * )
880  *
881  * Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid
882  * comparators.
883  */
884 public struct VersionRange
885 {
886 	private Version m_versA;
887 	private Version m_versB;
888 	private bool m_inclusiveA = true; // A comparison > (true) or >= (false)
889 	private bool m_inclusiveB = true; // B comparison < (true) or <= (false)
890 
891 	/// Matches any version
892 	public static immutable Any = VersionRange(Version.minRelease, Version.maxRelease);
893 	/// Doesn't match any version
894 	public static immutable Invalid = VersionRange(Version.maxRelease, Version.minRelease);
895 
896 	///
897 	public int opCmp (in VersionRange o) const scope @safe
898 	{
899 		if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1;
900 		if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1;
901 		if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1;
902 		if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1;
903 		return 0;
904 	}
905 
906 	public bool matches (in Version v, VersionMatchMode mode = VersionMatchMode.standard)
907 		const scope @safe
908 	{
909 		if (m_versA.isBranch) {
910 			enforce(this.isExactVersion());
911 			return m_versA == v;
912 		}
913 
914 		if (v.isBranch)
915 			return m_versA == v;
916 
917 		if (m_versA == m_versB)
918 			return this.m_versA.matches(v, mode);
919 
920 		return doCmp(m_inclusiveA, m_versA, v) &&
921 			doCmp(m_inclusiveB, v, m_versB);
922 	}
923 
924 	/// Modify in place
925 	public void merge (const VersionRange o) @safe
926 	{
927 		int acmp = m_versA.opCmp(o.m_versA);
928 		int bcmp = m_versB.opCmp(o.m_versB);
929 
930 		this.m_inclusiveA = !m_inclusiveA && acmp >= 0 ? false : o.m_inclusiveA;
931 		this.m_versA = acmp > 0 ? m_versA : o.m_versA;
932 		this.m_inclusiveB = !m_inclusiveB && bcmp <= 0 ? false : o.m_inclusiveB;
933 		this.m_versB = bcmp < 0 ? m_versB : o.m_versB;
934 	}
935 
936 	/// Returns true $(I iff) the version range only matches a specific version.
937 	@property bool isExactVersion() const scope @safe
938 	{
939 		return this.m_versA == this.m_versB;
940 	}
941 
942 	/// Determines if this dependency specification matches arbitrary versions.
943 	/// This is true in particular for the `any` constant.
944 	public bool matchesAny() const scope @safe
945 	{
946 		return this.m_inclusiveA && this.m_inclusiveB
947 			&& this.m_versA == Version.minRelease
948 			&& this.m_versB == Version.maxRelease;
949 	}
950 
951 	unittest {
952 		assert(VersionRange.fromString("*").matchesAny);
953 		assert(!VersionRange.fromString(">0.0.0").matchesAny);
954 		assert(!VersionRange.fromString(">=1.0.0").matchesAny);
955 		assert(!VersionRange.fromString("<1.0.0").matchesAny);
956 	}
957 
958 	public static VersionRange fromString (string ves) @safe
959 	{
960 		static import std.string;
961 
962 		enforce(ves.length > 0);
963 
964 		if (ves == Dependency.ANY_IDENT) {
965 			// Any version is good.
966 			ves = ">=0.0.0";
967 		}
968 
969 		if (ves.startsWith("~>")) {
970 			// Shortcut: "~>x.y.z" variant. Last non-zero number will indicate
971 			// the base for this so something like this: ">=x.y.z <x.(y+1).z"
972 			ves = ves[2..$];
973 			return VersionRange(
974 				Version(expandVersion(ves)), Version(bumpVersion(ves) ~ "-0"),
975 				true, false);
976 		}
977 
978 		if (ves.startsWith("^")) {
979 			// Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes.
980 			// if 0.x.y, ==0.x.y
981 			// if x.y.z, >=x.y.z <(x+1).0.0-0
982 			// ^x.y is equivalent to ^x.y.0.
983 			ves = ves[1..$].expandVersion;
984 			return VersionRange(
985 				Version(ves), Version(bumpIncompatibleVersion(ves) ~ "-0"),
986 				true, false);
987 		}
988 
989 		if (ves[0] == Version.branchPrefix) {
990 			auto ver = Version(ves);
991 			return VersionRange(ver, ver, true, true);
992 		}
993 
994 		if (std..string.indexOf("><=", ves[0]) == -1) {
995 			auto ver = Version(ves);
996 			return VersionRange(ver, ver, true, true);
997 		}
998 
999 		auto cmpa = skipComp(ves);
1000 		size_t idx2 = std..string.indexOf(ves, " ");
1001 		if (idx2 == -1) {
1002 			if (cmpa == "<=" || cmpa == "<")
1003 				return VersionRange(Version.minRelease, Version(ves), true, (cmpa == "<="));
1004 
1005 			if (cmpa == ">=" || cmpa == ">")
1006 				return VersionRange(Version(ves), Version.maxRelease, (cmpa == ">="), true);
1007 
1008 			// Converts "==" to ">=a&&<=a", which makes merging easier
1009 			return VersionRange(Version(ves), Version(ves), true, true);
1010 		}
1011 
1012 		enforce(cmpa == ">" || cmpa == ">=",
1013 				"First comparison operator expected to be either > or >=, not " ~ cmpa);
1014 		assert(ves[idx2] == ' ');
1015 		VersionRange ret;
1016 		ret.m_versA = Version(ves[0..idx2]);
1017 		ret.m_inclusiveA = cmpa == ">=";
1018 		string v2 = ves[idx2+1..$];
1019 		auto cmpb = skipComp(v2);
1020 		enforce(cmpb == "<" || cmpb == "<=",
1021 				"Second comparison operator expected to be either < or <=, not " ~ cmpb);
1022 		ret.m_versB = Version(v2);
1023 		ret.m_inclusiveB = cmpb == "<=";
1024 
1025 		enforce(!ret.m_versA.isBranch && !ret.m_versB.isBranch,
1026 				format("Cannot compare branches: %s", ves));
1027 		enforce(ret.m_versA <= ret.m_versB,
1028 				"First version must not be greater than the second one.");
1029 
1030 		return ret;
1031 	}
1032 
1033 	/// Returns a string representation of this range
1034 	string toString() const @safe {
1035 		static import std.string;
1036 
1037 		string r;
1038 
1039 		if (this == Invalid) return "no";
1040 		if (this.isExactVersion() && m_inclusiveA && m_inclusiveB) {
1041 			// Special "==" case
1042 			if (m_versA == Version.masterBranch) return "~master";
1043 			else return m_versA.toString();
1044 		}
1045 
1046 		// "~>", "^" case
1047 		if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) {
1048 			auto vs = m_versA.toString();
1049 			auto i1 = std..string.indexOf(vs, '-'), i2 = std..string.indexOf(vs, '+');
1050 			auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2;
1051 			auto va = i12 >= 0 ? vs[0 .. i12] : vs;
1052 			auto parts = va.splitter('.').array;
1053 			assert(parts.length == 3, "Version string with a digit group count != 3: "~va);
1054 
1055 			foreach (i; 0 .. 3) {
1056 				auto vp = parts[0 .. i+1].join(".");
1057 				auto ve = Version(expandVersion(vp));
1058 				auto veb = Version(bumpVersion(vp) ~ "-0");
1059 				if (ve == m_versA && veb == m_versB) return "~>" ~ vp;
1060 
1061 				auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0");
1062 				if (ve == m_versA && veb2 == m_versB) return "^" ~ vp;
1063 			}
1064 		}
1065 
1066 		if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString();
1067 		if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString();
1068 		if (this.matchesAny()) r = ">=0.0.0";
1069 		return r;
1070 	}
1071 
1072 	public bool isValid() const @safe {
1073 		return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB);
1074 	}
1075 
1076 	private static bool doCmp(bool inclusive, in Version a, in Version b)
1077 		@safe
1078 	{
1079 		return inclusive ? a <= b : a < b;
1080 	}
1081 
1082 	private static bool isDigit(char ch) @safe { return ch >= '0' && ch <= '9'; }
1083 	private static string skipComp(ref string c) @safe {
1084 		size_t idx = 0;
1085 		while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++;
1086 		enforce(idx < c.length, "Expected version number in version spec: "~c);
1087 		string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx];
1088 		c = c[idx..$];
1089 		switch(cmp) {
1090 			default: enforce(false, "No/Unknown comparison specified: '"~cmp~"'"); return ">=";
1091 			case ">=": goto case; case ">": goto case;
1092 			case "<=": goto case; case "<": goto case;
1093 			case "==": return cmp;
1094 		}
1095 	}
1096 }
1097 
1098 enum VersionMatchMode {
1099 	standard,  /// Match according to SemVer rules
1100 	strict     /// Also include build metadata suffix in the comparison
1101 }
1102 
1103 unittest {
1104 	Version a, b;
1105 
1106 	assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed");
1107 	assert(!a.isBranch, "Error: '1.0.0' treated as branch");
1108 	assert(a == a, "a == a failed");
1109 
1110 	assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed");
1111 	assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch");
1112 	assert(a.isMaster);
1113 	assert(a == Version.masterBranch, "Constructed master version != default master version.");
1114 
1115 	assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed.");
1116 	assert(a.isBranch, "Error: '~BRANCH' not treated as branch'");
1117 	assert(!a.isMaster);
1118 	assert(a == a, "a == a with branch failed");
1119 
1120 	// opCmp
1121 	a = Version("1.0.0");
1122 	b = Version("1.0.0");
1123 	assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed");
1124 	b = Version("2.0.0");
1125 	assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed");
1126 
1127 	a = Version.masterBranch;
1128 	b = Version("~BRANCH");
1129 	assert(a != b, "a != b with a:MASTER, b:'~branch' failed");
1130 	assert(a > b);
1131 	assert(a < Version("0.0.0"));
1132 	assert(b < Version("0.0.0"));
1133 	assert(a > Version("~Z"));
1134 	assert(b < Version("~Z"));
1135 
1136 	// SemVer 2.0.0-rc.2
1137 	a = Version("2.0.0-rc.2");
1138 	b = Version("2.0.0-rc.3");
1139 	assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3");
1140 
1141 	a = Version("2.0.0-rc.2+build-metadata");
1142 	b = Version("2.0.0+build-metadata");
1143 	assert(a < b, "Failed: "~a.toString()~"<"~b.toString());
1144 
1145 	// 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0
1146 	Version[] versions;
1147 	versions ~= Version("1.0.0-alpha");
1148 	versions ~= Version("1.0.0-alpha.1");
1149 	versions ~= Version("1.0.0-beta.2");
1150 	versions ~= Version("1.0.0-beta.11");
1151 	versions ~= Version("1.0.0-rc.1");
1152 	versions ~= Version("1.0.0");
1153 	for(int i=1; i<versions.length; ++i)
1154 		for(int j=i-1; j>=0; --j)
1155 			assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString());
1156 
1157 	assert(Version("1.0.0+a") == Version("1.0.0+b"));
1158 
1159 	assert(Version("1.0.0").matches(Version("1.0.0+foo")));
1160 	assert(Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard));
1161 	assert(!Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
1162 	assert(Version("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
1163 }
1164 
1165 /// Determines whether the given string is a Git hash.
1166 bool isGitHash(string hash) @nogc nothrow pure @safe
1167 {
1168 	import std.ascii : isHexDigit;
1169 	import std.utf : byCodeUnit;
1170 
1171 	return hash.length >= 7 && hash.length <= 40 && hash.byCodeUnit.all!isHexDigit;
1172 }
1173 
1174 @nogc nothrow pure @safe unittest {
1175 	assert(isGitHash("73535568b79a0b124bc1653002637a830ce0fcb8"));
1176 	assert(!isGitHash("735"));
1177 	assert(!isGitHash("73535568b79a0b124bc1-53002637a830ce0fcb8"));
1178 	assert(!isGitHash("73535568b79a0b124bg1"));
1179 }