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