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 				json["path"] = v.toString();
361 			},
362 
363 			(const Repository v) @trusted {
364 				initJson(json, optional, default_);
365 				json["repository"] = v.toString();
366 				json["version"] = v.m_ref;
367 			},
368 
369 			(const VersionRange v) @trusted {
370 				if (!selections && (optional || default_))
371 				{
372 					initJson(json, optional, default_);
373 					json["version"] = v.toString();
374 				}
375 				else
376 					json = Json(v.toString());
377 			},
378 		);
379 		return json;
380 	}
381 
382 	@trusted unittest {
383 		Dependency d = Dependency("==1.0.0");
384 		assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString());
385 		d = fromJson((fromJson(d.toJson())).toJson());
386 		assert(d == Dependency("1.0.0"));
387 		assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString());
388 	}
389 
390 	@trusted unittest {
391 		Dependency dependency = Dependency(Repository("git+http://localhost", "1.0.0"));
392 		Json expected = Json([
393 			"repository": Json("git+http://localhost"),
394 			"version": Json("1.0.0")
395 		]);
396 		assert(dependency.toJson() == expected, "Failed: " ~ dependency.toJson().toPrettyString());
397 	}
398 
399 	@trusted unittest {
400 		Dependency d = Dependency(NativePath("dir"));
401 		Json expected = Json([ "path": Json("dir") ]);
402 		assert(d.toJson() == expected, "Failed: " ~ d.toJson().toPrettyString());
403 	}
404 
405 	/** Constructs a new `Dependency` from its JSON representation.
406 
407 		See `toJson` for a description of the JSON format.
408 	*/
409 	static Dependency fromJson(Json verspec)
410 	@trusted { // NOTE Path and Json is @system in vibe.d 0.7.x and in the compatibility layer
411 		Dependency dep;
412 		if( verspec.type == Json.Type.object ){
413 			if( auto pp = "path" in verspec ) {
414 				dep = Dependency(NativePath(verspec["path"].get!string));
415 			} else if (auto repository = "repository" in verspec) {
416 				enforce("version" in verspec, "No version field specified!");
417 				enforce(repository.length > 0, "No repository field specified!");
418 
419 				dep = Dependency(Repository(
420                                      repository.get!string, verspec["version"].get!string));
421 			} else {
422 				enforce("version" in verspec, "No version field specified!");
423 				auto ver = verspec["version"].get!string;
424 				// Using the string to be able to specify a range of versions.
425 				dep = Dependency(ver);
426 			}
427 
428 			if (auto po = "optional" in verspec) dep.optional = po.get!bool;
429 			if (auto po = "default" in verspec) dep.default_ = po.get!bool;
430 		} else {
431 			// canonical "package-id": "version"
432 			dep = Dependency(verspec.get!string);
433 		}
434 		return dep;
435 	}
436 
437 	@trusted unittest {
438 		assert(fromJson(parseJsonString("\">=1.0.0 <2.0.0\"")) == Dependency(">=1.0.0 <2.0.0"));
439 		Dependency parsed = fromJson(parseJsonString(`
440 		{
441 			"version": "2.0.0",
442 			"optional": true,
443 			"default": true,
444 			"path": "path/to/package"
445 		}
446 			`));
447 		Dependency d = NativePath("path/to/package"); // supposed to ignore the version spec
448 		d.optional = true;
449 		d.default_ = true;
450 		assert(d == parsed);
451 	}
452 
453 	/** Compares dependency specifications.
454 
455 		These methods are suitable for equality comparisons, as well as for
456 		using `Dependency` as a key in hash or tree maps.
457 	*/
458 	bool opEquals(in Dependency o) const scope @safe {
459 		if (o.m_optional != this.m_optional) return false;
460 		if (o.m_default  != this.m_default)  return false;
461 		return this.m_value == o.m_value;
462 	}
463 
464 	/// ditto
465 	int opCmp(in Dependency o) const @safe {
466 		alias ResultMatch = match!(
467 			(VersionRange r1, VersionRange r2) => r1.opCmp(r2),
468 			(_1, _2) => 0,
469 		);
470 		if (auto result = ResultMatch(this.m_value, o.m_value))
471 			return result;
472 		if (m_optional != o.m_optional) return m_optional ? -1 : 1;
473 		return 0;
474 	}
475 
476 	/** Determines if this dependency specification is valid.
477 
478 		A specification is valid if it can match at least one version.
479 	*/
480 	bool valid() const @safe {
481 		return this.m_value.match!(
482 			(NativePath v) => true,
483 			(Repository v) => true,
484 			(VersionRange v) => v.isValid(),
485 		);
486 	}
487 
488 	/** Determines if this dependency specification matches arbitrary versions.
489 
490 		This is true in particular for the `any` constant.
491 	*/
492 	deprecated("Use `VersionRange.matchesAny` directly")
493 	bool matchesAny() const scope @safe {
494 		return this.m_value.match!(
495 			(NativePath v) => true,
496 			(Repository v) => true,
497 			(VersionRange v) => v.matchesAny(),
498 		);
499 	}
500 
501 	/** Tests if the specification matches a specific version.
502 	*/
503 	bool matches(string vers, VersionMatchMode mode = VersionMatchMode.standard) const @safe
504 	{
505 		return matches(Version(vers), mode);
506 	}
507 	/// ditto
508 	bool matches(in  Version v, VersionMatchMode mode = VersionMatchMode.standard) const @safe {
509 		return this.m_value.match!(
510 			(NativePath i) => true,
511 			(Repository i) => true,
512 			(VersionRange i) => i.matchesAny() || i.matches(v, mode),
513 		);
514 	}
515 
516 	/** Merges two dependency specifications.
517 
518 		The result is a specification that matches the intersection of the set
519 		of versions matched by the individual specifications. Note that this
520 		result can be invalid (i.e. not match any version).
521 	*/
522 	Dependency merge(ref const(Dependency) o) const @trusted {
523 		alias Merger = match!(
524 			(const NativePath a, const NativePath b) => a == b ? this : Invalid,
525 			(const NativePath a,       any         ) => o,
526 			(      any         , const NativePath b) => this,
527 
528 			(const Repository a, const Repository b) => a.m_ref == b.m_ref ? this : Invalid,
529 			(const Repository a,       any         ) => this,
530 			(      any         , const Repository b) => o,
531 
532 			(const VersionRange a, const VersionRange b) {
533 				if (a.matchesAny()) return o;
534 				if (b.matchesAny()) return this;
535 
536 				VersionRange copy = a;
537 				copy.merge(b);
538 				if (!copy.isValid()) return Invalid;
539 				return Dependency(copy);
540 			}
541 		);
542 
543 		Dependency ret = Merger(this.m_value, o.m_value);
544 		ret.m_optional = m_optional && o.m_optional;
545 		return ret;
546 	}
547 }
548 
549 /// Allow direct access to the underlying dependency
550 public auto visit (Handlers...) (const auto ref Dependency dep)
551 {
552     return dep.m_value.match!(Handlers);
553 }
554 
555 //// Ditto
556 public auto visit (Handlers...) (auto ref Dependency dep)
557 {
558     return dep.m_value.match!(Handlers);
559 }
560 
561 
562 unittest {
563 	Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0");
564 	assert (a.merge(b).valid() && a.merge(b).toString() == ">=1.3.0", a.merge(b).toString());
565 
566 	assertThrown(Dependency("<=2.0.0 >=1.0.0"));
567 	assertThrown(Dependency(">=2.0.0 <=1.0.0"));
568 
569 	a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0");
570 	assert (a.merge(b).valid() && a.merge(b).toString() == ">=2.0.0 <=5.0.0", a.merge(b).toString());
571 
572 	assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid");
573 
574 	a = Dependency(">1.0.0"); b = Dependency("<2.0.0");
575 	assert (a.merge(b).valid(), a.merge(b).toString());
576 	assert (a.merge(b).toString() == ">1.0.0 <2.0.0", a.merge(b).toString());
577 
578 	a = Dependency(">2.0.0"); b = Dependency("<1.0.0");
579 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
580 
581 	a = Dependency(">=2.0.0"); b = Dependency("<=1.0.0");
582 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
583 
584 	a = Dependency("==2.0.0"); b = Dependency("==1.0.0");
585 	assert (!(a.merge(b)).valid(), a.merge(b).toString());
586 
587 	a = Dependency("1.0.0"); b = Dependency("==1.0.0");
588 	assert (a == b);
589 
590 	a = Dependency("<=2.0.0"); b = Dependency("==1.0.0");
591 	Dependency m = a.merge(b);
592 	assert (m.valid(), m.toString());
593 	assert (m.matches(Version("1.0.0")));
594 	assert (!m.matches(Version("1.1.0")));
595 	assert (!m.matches(Version("0.0.1")));
596 
597 
598 	// branches / head revisions
599 	a = Dependency(Version.masterBranch);
600 	assert(a.valid());
601 	assert(a.matches(Version.masterBranch));
602 	b = Dependency(Version.masterBranch);
603 	m = a.merge(b);
604 	assert(m.matches(Version.masterBranch));
605 
606 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid");
607 	assertThrown(a = Dependency(">=1.0.0 " ~ Version.masterBranch.toString()), "Construction invalid");
608 
609 	immutable string branch1 = Version.branchPrefix ~ "Branch1";
610 	immutable string branch2 = Version.branchPrefix ~ "Branch2";
611 
612 	//assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded");
613 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded");
614 
615 	a = Dependency(branch1);
616 	b = Dependency(branch2);
617 	assert(!a.merge(b).valid, "Shouldn't be able to merge to different branches");
618 	b = a.merge(a);
619 	assert(b.valid, "Should be able to merge the same branches. (?)");
620 	assert(a == b);
621 
622 	a = Dependency(branch1);
623 	assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'");
624 	assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')");
625 	assert(!a.matches(Version.masterBranch), "Dependency(branch1) matches Version.masterBranch");
626 	assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'");
627 	assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'");
628 	a = Dependency(">=1.0.0");
629 	assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'");
630 
631 	// Testing optional dependencies.
632 	a = Dependency(">=1.0.0");
633 	assert(!a.optional, "Default is not optional.");
634 	b = a;
635 	assert(!a.merge(b).optional, "Merging two not optional dependencies wrong.");
636 	a.optional = true;
637 	assert(!a.merge(b).optional, "Merging optional with not optional wrong.");
638 	b.optional = true;
639 	assert(a.merge(b).optional, "Merging two optional dependencies wrong.");
640 
641 	// SemVer's sub identifiers.
642 	a = Dependency(">=1.0.0-beta");
643 	assert(!a.matches(Version("1.0.0-alpha")), "Failed: match 1.0.0-alpha with >=1.0.0-beta");
644 	assert(a.matches(Version("1.0.0-beta")), "Failed: match 1.0.0-beta with >=1.0.0-beta");
645 	assert(a.matches(Version("1.0.0")), "Failed: match 1.0.0 with >=1.0.0-beta");
646 	assert(a.matches(Version("1.0.0-rc")), "Failed: match 1.0.0-rc with >=1.0.0-beta");
647 
648 	// Approximate versions.
649 	a = Dependency("~>3.0");
650 	b = Dependency(">=3.0.0 <4.0.0-0");
651 	assert(a == b, "Testing failed: " ~ a.toString());
652 	assert(a.matches(Version("3.1.146")), "Failed: Match 3.1.146 with ~>0.1.2");
653 	assert(!a.matches(Version("0.2.0")), "Failed: Match 0.2.0 with ~>0.1.2");
654 	assert(!a.matches(Version("4.0.0-beta.1")));
655 	a = Dependency("~>3.0.0");
656 	assert(a == Dependency(">=3.0.0 <3.1.0-0"), "Testing failed: " ~ a.toString());
657 	a = Dependency("~>3.5");
658 	assert(a == Dependency(">=3.5.0 <4.0.0-0"), "Testing failed: " ~ a.toString());
659 	a = Dependency("~>3.5.0");
660 	assert(a == Dependency(">=3.5.0 <3.6.0-0"), "Testing failed: " ~ a.toString());
661 	assert(!Dependency("~>3.0.0").matches(Version("3.1.0-beta")));
662 
663 	a = Dependency("^0.1.2");
664 	assert(a == Dependency(">=0.1.2 <0.1.3-0"));
665 	a = Dependency("^1.2.3");
666 	assert(a == Dependency(">=1.2.3 <2.0.0-0"), "Testing failed: " ~ a.toString());
667 	a = Dependency("^1.2");
668 	assert(a == Dependency(">=1.2.0 <2.0.0-0"), "Testing failed: " ~ a.toString());
669 
670 	a = Dependency("~>0.1.1");
671 	b = Dependency("==0.1.0");
672 	assert(!a.merge(b).valid);
673 	b = Dependency("==0.1.9999");
674 	assert(a.merge(b).valid);
675 	b = Dependency("==0.2.0");
676 	assert(!a.merge(b).valid);
677 	b = Dependency("==0.2.0-beta.1");
678 	assert(!a.merge(b).valid);
679 
680 	a = Dependency("~>1.0.1-beta");
681 	b = Dependency(">=1.0.1-beta <1.1.0-0");
682 	assert(a == b, "Testing failed: " ~ a.toString());
683 	assert(a.matches(Version("1.0.1-beta")));
684 	assert(a.matches(Version("1.0.1-beta.6")));
685 
686 	a = Dependency("~d2test");
687 	assert(!a.optional);
688 	assert(a.valid);
689 	assert(a.version_ == Version("~d2test"));
690 
691 	a = Dependency("==~d2test");
692 	assert(!a.optional);
693 	assert(a.valid);
694 	assert(a.version_ == Version("~d2test"));
695 
696 	a = Dependency.Any;
697 	assert(!a.optional);
698 	assert(a.valid);
699 	assertThrown(a.version_);
700 	assert(a.matches(Version.masterBranch));
701 	assert(a.matches(Version("1.0.0")));
702 	assert(a.matches(Version("0.0.1-pre")));
703 	b = Dependency(">=1.0.1");
704 	assert(b == a.merge(b));
705 	assert(b == b.merge(a));
706 	b = Dependency(Version.masterBranch);
707 	assert(a.merge(b) == b);
708 	assert(b.merge(a) == b);
709 
710 	a.optional = true;
711 	assert(a.matches(Version.masterBranch));
712 	assert(a.matches(Version("1.0.0")));
713 	assert(a.matches(Version("0.0.1-pre")));
714 	b = Dependency(">=1.0.1");
715 	assert(b == a.merge(b));
716 	assert(b == b.merge(a));
717 	b = Dependency(Version.masterBranch);
718 	assert(a.merge(b) == b);
719 	assert(b.merge(a) == b);
720 
721 	assert(Dependency("1.0.0").matches(Version("1.0.0+foo")));
722 	assert(Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard));
723 	assert(!Dependency("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
724 	assert(Dependency("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
725 	assert(Dependency("~>1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
726 	assert(Dependency("~>1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
727 }
728 
729 unittest {
730 	assert(VersionRange.fromString("~>1.0.4").toString() == "~>1.0.4");
731 	assert(VersionRange.fromString("~>1.4").toString() == "~>1.4");
732 	// https://github.com/dlang/dub/issues/2830
733 	assert(VersionRange.fromString("~>2").toString() == "~>2.0");
734 	assert(VersionRange.fromString("~>5.0").toString() == "~>5.0");
735 
736 	assert(VersionRange.fromString("~>1.0.4+1.2.3").toString() == "~>1.0.4");
737 	assert(VersionRange.fromString("^0.1.2").toString() == "^0.1.2");
738 	assert(VersionRange.fromString("^1.2.3").toString() == "^1.2.3");
739 	assert(VersionRange.fromString("^1.2").toString() == "~>1.2"); // equivalent; prefer ~>
740 }
741 
742 /**
743 	Represents an SCM repository.
744 */
745 struct Repository
746 {
747 	private string m_remote;
748 	private string m_ref;
749 
750 	private Kind m_kind;
751 
752 	enum Kind
753 	{
754 		git,
755 	}
756 
757 	/**
758 		Params:
759 			remote = Repository remote.
760 			ref_   = Reference to use (SHA1, tag, branch name...)
761 	 */
762 	this(string remote, string ref_)
763 	{
764 		enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
765 
766 		m_remote = remote["git+".length .. $];
767 		m_kind = Kind.git;
768 		m_ref = ref_;
769 		assert(m_remote.length);
770 		assert(m_ref.length);
771 	}
772 
773 	/// Ditto
774 	deprecated("Use the constructor accepting a second parameter named `ref_`")
775 	this(string remote)
776 	{
777 		enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
778 
779 		m_remote = remote["git+".length .. $];
780 		m_kind = Kind.git;
781 		assert(m_remote.length);
782 	}
783 
784 	string toString() const nothrow pure @safe
785 	{
786 		if (empty) return null;
787 		string kindRepresentation;
788 
789 		final switch (kind)
790 		{
791 			case Kind.git:
792 				kindRepresentation = "git";
793 		}
794 		return kindRepresentation~"+"~remote;
795 	}
796 
797 	/**
798 		Returns:
799 			Repository URL or path.
800 	*/
801 	@property string remote() const @nogc nothrow pure @safe
802 	in { assert(m_remote !is null); }
803 	do
804 	{
805 		return m_remote;
806 	}
807 
808 	/**
809 		Returns:
810 			The reference (commit hash, branch name, tag) we are targeting
811 	*/
812 	@property string ref_() const @nogc nothrow pure @safe
813 	in { assert(m_remote !is null); }
814 	in { assert(m_ref !is null); }
815 	do
816 	{
817 		return m_ref;
818 	}
819 
820 	/**
821 		Returns:
822 			Repository type.
823 	*/
824 	@property Kind kind() const @nogc nothrow pure @safe
825 	{
826 		return m_kind;
827 	}
828 
829 	/**
830 		Returns:
831 			Whether the repository was initialized with an URL or path.
832 	*/
833 	@property bool empty() const @nogc nothrow pure @safe
834 	{
835 		return m_remote.empty;
836 	}
837 }
838 
839 
840 /**
841 	Represents a version in semantic version format, or a branch identifier.
842 
843 	This can either have the form "~master", where "master" is a branch name,
844 	or the form "major.update.bugfix-prerelease+buildmetadata" (see the
845 	Semantic Versioning Specification v2.0.0 at http://semver.org/).
846 */
847 struct Version {
848 	private {
849 		static immutable MAX_VERS = "99999.0.0";
850 		static immutable masterString = "~master";
851 		enum branchPrefix = '~';
852 		string m_version;
853 	}
854 
855 	static immutable Version minRelease = Version("0.0.0");
856 	static immutable Version maxRelease = Version(MAX_VERS);
857 	static immutable Version masterBranch = Version(masterString);
858 
859 	/** Constructs a new `Version` from its string representation.
860 	*/
861 	this(string vers) @safe pure
862 	{
863 		enforce(vers.length > 1, "Version strings must not be empty.");
864 		if (vers[0] != branchPrefix)
865 			enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers);
866 		m_version = vers;
867 	}
868 
869 	/** Constructs a new `Version` from its string representation.
870 
871 		This method is equivalent to calling the constructor and is used as an
872 		endpoint for the serialization framework.
873 	*/
874 	static Version fromString(string vers) @safe pure { return Version(vers); }
875 
876 	bool opEquals(in Version oth) const scope @safe pure
877 	{
878 		return opCmp(oth) == 0;
879 	}
880 
881 	/// Tests if this represents a branch instead of a version.
882 	@property bool isBranch() const scope @safe pure nothrow @nogc
883 	{
884 		return m_version.length > 0 && m_version[0] == branchPrefix;
885 	}
886 
887 	/// Tests if this represents the master branch "~master".
888 	@property bool isMaster() const scope @safe pure nothrow @nogc
889 	{
890 		return m_version == masterString;
891 	}
892 
893 	/** Tests if this represents a pre-release version.
894 
895 		Note that branches are always considered pre-release versions.
896 	*/
897 	@property bool isPreRelease() const scope @safe pure nothrow @nogc
898 	{
899 		if (isBranch) return true;
900 		return isPreReleaseVersion(m_version);
901 	}
902 
903 	/** Tests two versions for equality, according to the selected match mode.
904 	*/
905 	bool matches(in Version other, VersionMatchMode mode = VersionMatchMode.standard)
906 	const scope @safe pure
907 	{
908 		if (mode == VersionMatchMode.strict)
909 			return this.toString() == other.toString();
910 		return this == other;
911 	}
912 
913 	/** Compares two versions/branches for precedence.
914 
915 		Versions generally have precedence over branches and the master branch
916 		has precedence over other branches. Apart from that, versions are
917 		compared using SemVer semantics, while branches are compared
918 		lexicographically.
919 	*/
920 	int opCmp(in Version other) const scope @safe pure
921 	{
922 		if (isBranch || other.isBranch) {
923 			if(m_version == other.m_version) return 0;
924 			if (!isBranch) return 1;
925 			else if (!other.isBranch) return -1;
926 			if (isMaster) return 1;
927 			else if (other.isMaster) return -1;
928 			return this.m_version < other.m_version ? -1 : 1;
929 		}
930 
931 		return compareVersions(m_version, other.m_version);
932 	}
933 
934 	/// Returns the string representation of the version/branch.
935 	string toString() const return scope @safe pure nothrow @nogc
936 	{
937 		return m_version;
938 	}
939 }
940 
941 /**
942  * A range of versions that are acceptable
943  *
944  * While not directly described in SemVer v2.0.0, a common set
945  * of range operators have appeared among package managers.
946  * We mostly NPM's: https://semver.npmjs.com/
947  *
948  * Hence the acceptable forms for this string are as follows:
949  *
950  * $(UL
951  *  $(LI `"1.0.0"` - a single version in SemVer format)
952  *  $(LI `"==1.0.0"` - alternative single version notation)
953  *  $(LI `">1.0.0"` - version range with a single bound)
954  *  $(LI `">1.0.0 <2.0.0"` - version range with two bounds)
955  *  $(LI `"~>1.0.0"` - a fuzzy version range)
956  *  $(LI `"~>1.0"` - a fuzzy version range with partial version)
957  *  $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z))
958  *  $(LI `"^1.0"` - same as ^1.0.0)
959  *  $(LI `"~master"` - a branch name)
960  *  $(LI `"*"` - match any version (see also `VersionRange.Any`))
961  * )
962  *
963  * Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid
964  * comparators.
965  */
966 public struct VersionRange
967 {
968 	private Version m_versA;
969 	private Version m_versB;
970 	private bool m_inclusiveA = true; // A comparison > (true) or >= (false)
971 	private bool m_inclusiveB = true; // B comparison < (true) or <= (false)
972 
973 	/// Matches any version
974 	public static immutable Any = VersionRange(Version.minRelease, Version.maxRelease);
975 	/// Doesn't match any version
976 	public static immutable Invalid = VersionRange(Version.maxRelease, Version.minRelease);
977 
978 	///
979 	public int opCmp (in VersionRange o) const scope @safe
980 	{
981 		if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1;
982 		if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1;
983 		if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1;
984 		if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1;
985 		return 0;
986 	}
987 
988 	public bool matches (in Version v, VersionMatchMode mode = VersionMatchMode.standard)
989 		const scope @safe
990 	{
991 		if (m_versA.isBranch) {
992 			enforce(this.isExactVersion());
993 			return m_versA == v;
994 		}
995 
996 		if (v.isBranch)
997 			return m_versA == v;
998 
999 		if (m_versA == m_versB)
1000 			return this.m_versA.matches(v, mode);
1001 
1002 		return doCmp(m_inclusiveA, m_versA, v) &&
1003 			doCmp(m_inclusiveB, v, m_versB);
1004 	}
1005 
1006 	/// Modify in place
1007 	public void merge (const VersionRange o) @safe
1008 	{
1009 		int acmp = m_versA.opCmp(o.m_versA);
1010 		int bcmp = m_versB.opCmp(o.m_versB);
1011 
1012 		this.m_inclusiveA = !m_inclusiveA && acmp >= 0 ? false : o.m_inclusiveA;
1013 		this.m_versA = acmp > 0 ? m_versA : o.m_versA;
1014 		this.m_inclusiveB = !m_inclusiveB && bcmp <= 0 ? false : o.m_inclusiveB;
1015 		this.m_versB = bcmp < 0 ? m_versB : o.m_versB;
1016 	}
1017 
1018 	/// Returns true $(I iff) the version range only matches a specific version.
1019 	@property bool isExactVersion() const scope @safe
1020 	{
1021 		return this.m_versA == this.m_versB;
1022 	}
1023 
1024 	/// Determines if this dependency specification matches arbitrary versions.
1025 	/// This is true in particular for the `any` constant.
1026 	public bool matchesAny() const scope @safe
1027 	{
1028 		return this.m_inclusiveA && this.m_inclusiveB
1029 			&& this.m_versA == Version.minRelease
1030 			&& this.m_versB == Version.maxRelease;
1031 	}
1032 
1033 	unittest {
1034 		assert(VersionRange.fromString("*").matchesAny);
1035 		assert(!VersionRange.fromString(">0.0.0").matchesAny);
1036 		assert(!VersionRange.fromString(">=1.0.0").matchesAny);
1037 		assert(!VersionRange.fromString("<1.0.0").matchesAny);
1038 	}
1039 
1040 	public static VersionRange fromString (string ves) @safe
1041 	{
1042 		static import std.string;
1043 
1044 		enforce(ves.length > 0);
1045 
1046 		if (ves == Dependency.ANY_IDENT) {
1047 			// Any version is good.
1048 			ves = ">=0.0.0";
1049 		}
1050 
1051 		if (ves.startsWith("~>")) {
1052 			// Shortcut: "~>x.y.z" variant. Last non-zero number will indicate
1053 			// the base for this so something like this: ">=x.y.z <x.(y+1).z"
1054 			ves = ves[2..$];
1055 			return VersionRange(
1056 				Version(expandVersion(ves)), Version(bumpVersion(ves) ~ "-0"),
1057 				true, false);
1058 		}
1059 
1060 		if (ves.startsWith("^")) {
1061 			// Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes.
1062 			// if 0.x.y, ==0.x.y
1063 			// if x.y.z, >=x.y.z <(x+1).0.0-0
1064 			// ^x.y is equivalent to ^x.y.0.
1065 			ves = ves[1..$].expandVersion;
1066 			return VersionRange(
1067 				Version(ves), Version(bumpIncompatibleVersion(ves) ~ "-0"),
1068 				true, false);
1069 		}
1070 
1071 		if (ves[0] == Version.branchPrefix) {
1072 			auto ver = Version(ves);
1073 			return VersionRange(ver, ver, true, true);
1074 		}
1075 
1076 		if (std..string.indexOf("><=", ves[0]) == -1) {
1077 			auto ver = Version(ves);
1078 			return VersionRange(ver, ver, true, true);
1079 		}
1080 
1081 		auto cmpa = skipComp(ves);
1082 		size_t idx2 = std..string.indexOf(ves, " ");
1083 		if (idx2 == -1) {
1084 			if (cmpa == "<=" || cmpa == "<")
1085 				return VersionRange(Version.minRelease, Version(ves), true, (cmpa == "<="));
1086 
1087 			if (cmpa == ">=" || cmpa == ">")
1088 				return VersionRange(Version(ves), Version.maxRelease, (cmpa == ">="), true);
1089 
1090 			// Converts "==" to ">=a&&<=a", which makes merging easier
1091 			return VersionRange(Version(ves), Version(ves), true, true);
1092 		}
1093 
1094 		enforce(cmpa == ">" || cmpa == ">=",
1095 				"First comparison operator expected to be either > or >=, not " ~ cmpa);
1096 		assert(ves[idx2] == ' ');
1097 		VersionRange ret;
1098 		ret.m_versA = Version(ves[0..idx2]);
1099 		ret.m_inclusiveA = cmpa == ">=";
1100 		string v2 = ves[idx2+1..$];
1101 		auto cmpb = skipComp(v2);
1102 		enforce(cmpb == "<" || cmpb == "<=",
1103 				"Second comparison operator expected to be either < or <=, not " ~ cmpb);
1104 		ret.m_versB = Version(v2);
1105 		ret.m_inclusiveB = cmpb == "<=";
1106 
1107 		enforce(!ret.m_versA.isBranch && !ret.m_versB.isBranch,
1108 				format("Cannot compare branches: %s", ves));
1109 		enforce(ret.m_versA <= ret.m_versB,
1110 				"First version must not be greater than the second one.");
1111 
1112 		return ret;
1113 	}
1114 
1115 	/// Returns a string representation of this range
1116 	string toString() const @safe {
1117 		static import std.string;
1118 
1119 		string r;
1120 
1121 		if (this == Invalid) return "no";
1122 		if (this.matchesAny()) return "*";
1123 		if (this.isExactVersion() && m_inclusiveA && m_inclusiveB) {
1124 			// Special "==" case
1125 			if (m_versA == Version.masterBranch) return "~master";
1126 			else return m_versA.toString();
1127 		}
1128 
1129 		// "~>", "^" case
1130 		if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) {
1131 			auto vs = m_versA.toString();
1132 			auto i1 = std..string.indexOf(vs, '-'), i2 = std..string.indexOf(vs, '+');
1133 			auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2;
1134 			auto va = i12 >= 0 ? vs[0 .. i12] : vs;
1135 			auto parts = va.splitter('.').array;
1136 			assert(parts.length == 3, "Version string with a digit group count != 3: "~va);
1137 
1138 			// Start at 1 because the notation `~>1` and `^1` are equivalent
1139 			// to `~>1.0` and `^1.0`, and the latter are better understood
1140 			// and recognized by users. See for example issue 2830.
1141 			foreach (i; 1 .. 3) {
1142 				auto vp = parts[0 .. i+1].join(".");
1143 				auto ve = Version(expandVersion(vp));
1144 				auto veb = Version(bumpVersion(vp) ~ "-0");
1145 				if (ve == m_versA && veb == m_versB) return "~>" ~ vp;
1146 
1147 				auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0");
1148 				if (ve == m_versA && veb2 == m_versB) return "^" ~ vp;
1149 			}
1150 		}
1151 
1152 		if (m_versA != Version.minRelease || !m_inclusiveA)
1153 			r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString();
1154 		if (m_versB != Version.maxRelease || !m_inclusiveB)
1155 			r ~= (r.length == 0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~
1156 				m_versB.toString();
1157 
1158 		return r;
1159 	}
1160 
1161 	public bool isValid() const @safe {
1162 		return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB);
1163 	}
1164 
1165 	private static bool doCmp(bool inclusive, in Version a, in Version b)
1166 		@safe
1167 	{
1168 		return inclusive ? a <= b : a < b;
1169 	}
1170 
1171 	private static bool isDigit(char ch) @safe { return ch >= '0' && ch <= '9'; }
1172 	private static string skipComp(ref string c) @safe {
1173 		size_t idx = 0;
1174 		while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++;
1175 		enforce(idx < c.length, "Expected version number in version spec: "~c);
1176 		string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx];
1177 		c = c[idx..$];
1178 		switch(cmp) {
1179 			default: enforce(false, "No/Unknown comparison specified: '"~cmp~"'"); return ">=";
1180 			case ">=": goto case; case ">": goto case;
1181 			case "<=": goto case; case "<": goto case;
1182 			case "==": return cmp;
1183 		}
1184 	}
1185 }
1186 
1187 enum VersionMatchMode {
1188 	standard,  /// Match according to SemVer rules
1189 	strict     /// Also include build metadata suffix in the comparison
1190 }
1191 
1192 unittest {
1193 	Version a, b;
1194 
1195 	assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed");
1196 	assert(!a.isBranch, "Error: '1.0.0' treated as branch");
1197 	assert(a == a, "a == a failed");
1198 
1199 	assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed");
1200 	assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch");
1201 	assert(a.isMaster);
1202 	assert(a == Version.masterBranch, "Constructed master version != default master version.");
1203 
1204 	assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed.");
1205 	assert(a.isBranch, "Error: '~BRANCH' not treated as branch'");
1206 	assert(!a.isMaster);
1207 	assert(a == a, "a == a with branch failed");
1208 
1209 	// opCmp
1210 	a = Version("1.0.0");
1211 	b = Version("1.0.0");
1212 	assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed");
1213 	b = Version("2.0.0");
1214 	assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed");
1215 
1216 	a = Version.masterBranch;
1217 	b = Version("~BRANCH");
1218 	assert(a != b, "a != b with a:MASTER, b:'~branch' failed");
1219 	assert(a > b);
1220 	assert(a < Version("0.0.0"));
1221 	assert(b < Version("0.0.0"));
1222 	assert(a > Version("~Z"));
1223 	assert(b < Version("~Z"));
1224 
1225 	// SemVer 2.0.0-rc.2
1226 	a = Version("2.0.0-rc.2");
1227 	b = Version("2.0.0-rc.3");
1228 	assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3");
1229 
1230 	a = Version("2.0.0-rc.2+build-metadata");
1231 	b = Version("2.0.0+build-metadata");
1232 	assert(a < b, "Failed: "~a.toString()~"<"~b.toString());
1233 
1234 	// 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
1235 	Version[] versions;
1236 	versions ~= Version("1.0.0-alpha");
1237 	versions ~= Version("1.0.0-alpha.1");
1238 	versions ~= Version("1.0.0-beta.2");
1239 	versions ~= Version("1.0.0-beta.11");
1240 	versions ~= Version("1.0.0-rc.1");
1241 	versions ~= Version("1.0.0");
1242 	for(int i=1; i<versions.length; ++i)
1243 		for(int j=i-1; j>=0; --j)
1244 			assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString());
1245 
1246 	assert(Version("1.0.0+a") == Version("1.0.0+b"));
1247 
1248 	assert(Version("1.0.0").matches(Version("1.0.0+foo")));
1249 	assert(Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard));
1250 	assert(!Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
1251 	assert(Version("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
1252 }
1253 
1254 // Erased version specification for dependency, converted to "" instead of ">0.0.0"
1255 // https://github.com/dlang/dub/issues/2901
1256 unittest
1257 {
1258     assert(VersionRange.fromString(">0.0.0").toString() == ">0.0.0");
1259 }
1260 
1261 /// Determines whether the given string is a Git hash.
1262 bool isGitHash(string hash) @nogc nothrow pure @safe
1263 {
1264 	import std.ascii : isHexDigit;
1265 	import std.utf : byCodeUnit;
1266 
1267 	return hash.length >= 7 && hash.length <= 40 && hash.byCodeUnit.all!isHexDigit;
1268 }
1269 
1270 @nogc nothrow pure @safe unittest {
1271 	assert(isGitHash("73535568b79a0b124bc1653002637a830ce0fcb8"));
1272 	assert(!isGitHash("735"));
1273 	assert(!isGitHash("73535568b79a0b124bc1-53002637a830ce0fcb8"));
1274 	assert(!isGitHash("73535568b79a0b124bg1"));
1275 }