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