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