Newer
Older
dub_jkp / source / dub / dependency.d
  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. assert(VersionRange.fromString("~>2").toString() == "~>2");
  733. assert(VersionRange.fromString("~>1.0.4+1.2.3").toString() == "~>1.0.4");
  734. assert(VersionRange.fromString("^0.1.2").toString() == "^0.1.2");
  735. assert(VersionRange.fromString("^1.2.3").toString() == "^1.2.3");
  736. assert(VersionRange.fromString("^1.2").toString() == "~>1.2"); // equivalent; prefer ~>
  737. }
  738.  
  739. /**
  740. Represents an SCM repository.
  741. */
  742. struct Repository
  743. {
  744. private string m_remote;
  745. private string m_ref;
  746.  
  747. private Kind m_kind;
  748.  
  749. enum Kind
  750. {
  751. git,
  752. }
  753.  
  754. /**
  755. Params:
  756. remote = Repository remote.
  757. ref_ = Reference to use (SHA1, tag, branch name...)
  758. */
  759. this(string remote, string ref_)
  760. {
  761. enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
  762.  
  763. m_remote = remote["git+".length .. $];
  764. m_kind = Kind.git;
  765. m_ref = ref_;
  766. assert(m_remote.length);
  767. assert(m_ref.length);
  768. }
  769.  
  770. /// Ditto
  771. deprecated("Use the constructor accepting a second parameter named `ref_`")
  772. this(string remote)
  773. {
  774. enforce(remote.startsWith("git+"), "Unsupported repository type (supports: git+URL)");
  775.  
  776. m_remote = remote["git+".length .. $];
  777. m_kind = Kind.git;
  778. assert(m_remote.length);
  779. }
  780.  
  781. string toString() const nothrow pure @safe
  782. {
  783. if (empty) return null;
  784. string kindRepresentation;
  785.  
  786. final switch (kind)
  787. {
  788. case Kind.git:
  789. kindRepresentation = "git";
  790. }
  791. return kindRepresentation~"+"~remote;
  792. }
  793.  
  794. /**
  795. Returns:
  796. Repository URL or path.
  797. */
  798. @property string remote() const @nogc nothrow pure @safe
  799. in { assert(m_remote !is null); }
  800. do
  801. {
  802. return m_remote;
  803. }
  804.  
  805. /**
  806. Returns:
  807. The reference (commit hash, branch name, tag) we are targeting
  808. */
  809. @property string ref_() const @nogc nothrow pure @safe
  810. in { assert(m_remote !is null); }
  811. in { assert(m_ref !is null); }
  812. do
  813. {
  814. return m_ref;
  815. }
  816.  
  817. /**
  818. Returns:
  819. Repository type.
  820. */
  821. @property Kind kind() const @nogc nothrow pure @safe
  822. {
  823. return m_kind;
  824. }
  825.  
  826. /**
  827. Returns:
  828. Whether the repository was initialized with an URL or path.
  829. */
  830. @property bool empty() const @nogc nothrow pure @safe
  831. {
  832. return m_remote.empty;
  833. }
  834. }
  835.  
  836.  
  837. /**
  838. Represents a version in semantic version format, or a branch identifier.
  839.  
  840. This can either have the form "~master", where "master" is a branch name,
  841. or the form "major.update.bugfix-prerelease+buildmetadata" (see the
  842. Semantic Versioning Specification v2.0.0 at http://semver.org/).
  843. */
  844. struct Version {
  845. private {
  846. static immutable MAX_VERS = "99999.0.0";
  847. static immutable masterString = "~master";
  848. enum branchPrefix = '~';
  849. string m_version;
  850. }
  851.  
  852. static immutable Version minRelease = Version("0.0.0");
  853. static immutable Version maxRelease = Version(MAX_VERS);
  854. static immutable Version masterBranch = Version(masterString);
  855.  
  856. /** Constructs a new `Version` from its string representation.
  857. */
  858. this(string vers) @safe pure
  859. {
  860. enforce(vers.length > 1, "Version strings must not be empty.");
  861. if (vers[0] != branchPrefix)
  862. enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers);
  863. m_version = vers;
  864. }
  865.  
  866. /** Constructs a new `Version` from its string representation.
  867.  
  868. This method is equivalent to calling the constructor and is used as an
  869. endpoint for the serialization framework.
  870. */
  871. static Version fromString(string vers) @safe pure { return Version(vers); }
  872.  
  873. bool opEquals(in Version oth) const scope @safe pure
  874. {
  875. return opCmp(oth) == 0;
  876. }
  877.  
  878. /// Tests if this represents a branch instead of a version.
  879. @property bool isBranch() const scope @safe pure nothrow @nogc
  880. {
  881. return m_version.length > 0 && m_version[0] == branchPrefix;
  882. }
  883.  
  884. /// Tests if this represents the master branch "~master".
  885. @property bool isMaster() const scope @safe pure nothrow @nogc
  886. {
  887. return m_version == masterString;
  888. }
  889.  
  890. /** Tests if this represents a pre-release version.
  891.  
  892. Note that branches are always considered pre-release versions.
  893. */
  894. @property bool isPreRelease() const scope @safe pure nothrow @nogc
  895. {
  896. if (isBranch) return true;
  897. return isPreReleaseVersion(m_version);
  898. }
  899.  
  900. /** Tests two versions for equality, according to the selected match mode.
  901. */
  902. bool matches(in Version other, VersionMatchMode mode = VersionMatchMode.standard)
  903. const scope @safe pure
  904. {
  905. if (mode == VersionMatchMode.strict)
  906. return this.toString() == other.toString();
  907. return this == other;
  908. }
  909.  
  910. /** Compares two versions/branches for precedence.
  911.  
  912. Versions generally have precedence over branches and the master branch
  913. has precedence over other branches. Apart from that, versions are
  914. compared using SemVer semantics, while branches are compared
  915. lexicographically.
  916. */
  917. int opCmp(in Version other) const scope @safe pure
  918. {
  919. if (isBranch || other.isBranch) {
  920. if(m_version == other.m_version) return 0;
  921. if (!isBranch) return 1;
  922. else if (!other.isBranch) return -1;
  923. if (isMaster) return 1;
  924. else if (other.isMaster) return -1;
  925. return this.m_version < other.m_version ? -1 : 1;
  926. }
  927.  
  928. return compareVersions(m_version, other.m_version);
  929. }
  930.  
  931. /// Returns the string representation of the version/branch.
  932. string toString() const return scope @safe pure nothrow @nogc
  933. {
  934. return m_version;
  935. }
  936. }
  937.  
  938. /**
  939. * A range of versions that are acceptable
  940. *
  941. * While not directly described in SemVer v2.0.0, a common set
  942. * of range operators have appeared among package managers.
  943. * We mostly NPM's: https://semver.npmjs.com/
  944. *
  945. * Hence the acceptable forms for this string are as follows:
  946. *
  947. * $(UL
  948. * $(LI `"1.0.0"` - a single version in SemVer format)
  949. * $(LI `"==1.0.0"` - alternative single version notation)
  950. * $(LI `">1.0.0"` - version range with a single bound)
  951. * $(LI `">1.0.0 <2.0.0"` - version range with two bounds)
  952. * $(LI `"~>1.0.0"` - a fuzzy version range)
  953. * $(LI `"~>1.0"` - a fuzzy version range with partial version)
  954. * $(LI `"^1.0.0"` - semver compatible version range (same version if 0.x.y, ==major >=minor.patch if x.y.z))
  955. * $(LI `"^1.0"` - same as ^1.0.0)
  956. * $(LI `"~master"` - a branch name)
  957. * $(LI `"*"` - match any version (see also `VersionRange.Any`))
  958. * )
  959. *
  960. * Apart from "$(LT)" and "$(GT)", "$(GT)=" and "$(LT)=" are also valid
  961. * comparators.
  962. */
  963. public struct VersionRange
  964. {
  965. private Version m_versA;
  966. private Version m_versB;
  967. private bool m_inclusiveA = true; // A comparison > (true) or >= (false)
  968. private bool m_inclusiveB = true; // B comparison < (true) or <= (false)
  969.  
  970. /// Matches any version
  971. public static immutable Any = VersionRange(Version.minRelease, Version.maxRelease);
  972. /// Doesn't match any version
  973. public static immutable Invalid = VersionRange(Version.maxRelease, Version.minRelease);
  974.  
  975. ///
  976. public int opCmp (in VersionRange o) const scope @safe
  977. {
  978. if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1;
  979. if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1;
  980. if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1;
  981. if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1;
  982. return 0;
  983. }
  984.  
  985. public bool matches (in Version v, VersionMatchMode mode = VersionMatchMode.standard)
  986. const scope @safe
  987. {
  988. if (m_versA.isBranch) {
  989. enforce(this.isExactVersion());
  990. return m_versA == v;
  991. }
  992.  
  993. if (v.isBranch)
  994. return m_versA == v;
  995.  
  996. if (m_versA == m_versB)
  997. return this.m_versA.matches(v, mode);
  998.  
  999. return doCmp(m_inclusiveA, m_versA, v) &&
  1000. doCmp(m_inclusiveB, v, m_versB);
  1001. }
  1002.  
  1003. /// Modify in place
  1004. public void merge (const VersionRange o) @safe
  1005. {
  1006. int acmp = m_versA.opCmp(o.m_versA);
  1007. int bcmp = m_versB.opCmp(o.m_versB);
  1008.  
  1009. this.m_inclusiveA = !m_inclusiveA && acmp >= 0 ? false : o.m_inclusiveA;
  1010. this.m_versA = acmp > 0 ? m_versA : o.m_versA;
  1011. this.m_inclusiveB = !m_inclusiveB && bcmp <= 0 ? false : o.m_inclusiveB;
  1012. this.m_versB = bcmp < 0 ? m_versB : o.m_versB;
  1013. }
  1014.  
  1015. /// Returns true $(I iff) the version range only matches a specific version.
  1016. @property bool isExactVersion() const scope @safe
  1017. {
  1018. return this.m_versA == this.m_versB;
  1019. }
  1020.  
  1021. /// Determines if this dependency specification matches arbitrary versions.
  1022. /// This is true in particular for the `any` constant.
  1023. public bool matchesAny() const scope @safe
  1024. {
  1025. return this.m_inclusiveA && this.m_inclusiveB
  1026. && this.m_versA == Version.minRelease
  1027. && this.m_versB == Version.maxRelease;
  1028. }
  1029.  
  1030. unittest {
  1031. assert(VersionRange.fromString("*").matchesAny);
  1032. assert(!VersionRange.fromString(">0.0.0").matchesAny);
  1033. assert(!VersionRange.fromString(">=1.0.0").matchesAny);
  1034. assert(!VersionRange.fromString("<1.0.0").matchesAny);
  1035. }
  1036.  
  1037. public static VersionRange fromString (string ves) @safe
  1038. {
  1039. static import std.string;
  1040.  
  1041. enforce(ves.length > 0);
  1042.  
  1043. if (ves == Dependency.ANY_IDENT) {
  1044. // Any version is good.
  1045. ves = ">=0.0.0";
  1046. }
  1047.  
  1048. if (ves.startsWith("~>")) {
  1049. // Shortcut: "~>x.y.z" variant. Last non-zero number will indicate
  1050. // the base for this so something like this: ">=x.y.z <x.(y+1).z"
  1051. ves = ves[2..$];
  1052. return VersionRange(
  1053. Version(expandVersion(ves)), Version(bumpVersion(ves) ~ "-0"),
  1054. true, false);
  1055. }
  1056.  
  1057. if (ves.startsWith("^")) {
  1058. // Shortcut: "^x.y.z" variant. "Semver compatible" - no breaking changes.
  1059. // if 0.x.y, ==0.x.y
  1060. // if x.y.z, >=x.y.z <(x+1).0.0-0
  1061. // ^x.y is equivalent to ^x.y.0.
  1062. ves = ves[1..$].expandVersion;
  1063. return VersionRange(
  1064. Version(ves), Version(bumpIncompatibleVersion(ves) ~ "-0"),
  1065. true, false);
  1066. }
  1067.  
  1068. if (ves[0] == Version.branchPrefix) {
  1069. auto ver = Version(ves);
  1070. return VersionRange(ver, ver, true, true);
  1071. }
  1072.  
  1073. if (std.string.indexOf("><=", ves[0]) == -1) {
  1074. auto ver = Version(ves);
  1075. return VersionRange(ver, ver, true, true);
  1076. }
  1077.  
  1078. auto cmpa = skipComp(ves);
  1079. size_t idx2 = std.string.indexOf(ves, " ");
  1080. if (idx2 == -1) {
  1081. if (cmpa == "<=" || cmpa == "<")
  1082. return VersionRange(Version.minRelease, Version(ves), true, (cmpa == "<="));
  1083.  
  1084. if (cmpa == ">=" || cmpa == ">")
  1085. return VersionRange(Version(ves), Version.maxRelease, (cmpa == ">="), true);
  1086.  
  1087. // Converts "==" to ">=a&&<=a", which makes merging easier
  1088. return VersionRange(Version(ves), Version(ves), true, true);
  1089. }
  1090.  
  1091. enforce(cmpa == ">" || cmpa == ">=",
  1092. "First comparison operator expected to be either > or >=, not " ~ cmpa);
  1093. assert(ves[idx2] == ' ');
  1094. VersionRange ret;
  1095. ret.m_versA = Version(ves[0..idx2]);
  1096. ret.m_inclusiveA = cmpa == ">=";
  1097. string v2 = ves[idx2+1..$];
  1098. auto cmpb = skipComp(v2);
  1099. enforce(cmpb == "<" || cmpb == "<=",
  1100. "Second comparison operator expected to be either < or <=, not " ~ cmpb);
  1101. ret.m_versB = Version(v2);
  1102. ret.m_inclusiveB = cmpb == "<=";
  1103.  
  1104. enforce(!ret.m_versA.isBranch && !ret.m_versB.isBranch,
  1105. format("Cannot compare branches: %s", ves));
  1106. enforce(ret.m_versA <= ret.m_versB,
  1107. "First version must not be greater than the second one.");
  1108.  
  1109. return ret;
  1110. }
  1111.  
  1112. /// Returns a string representation of this range
  1113. string toString() const @safe {
  1114. static import std.string;
  1115.  
  1116. string r;
  1117.  
  1118. if (this == Invalid) return "no";
  1119. if (this.isExactVersion() && m_inclusiveA && m_inclusiveB) {
  1120. // Special "==" case
  1121. if (m_versA == Version.masterBranch) return "~master";
  1122. else return m_versA.toString();
  1123. }
  1124.  
  1125. // "~>", "^" case
  1126. if (m_inclusiveA && !m_inclusiveB && !m_versA.isBranch) {
  1127. auto vs = m_versA.toString();
  1128. auto i1 = std.string.indexOf(vs, '-'), i2 = std.string.indexOf(vs, '+');
  1129. auto i12 = i1 >= 0 ? i2 >= 0 ? i1 < i2 ? i1 : i2 : i1 : i2;
  1130. auto va = i12 >= 0 ? vs[0 .. i12] : vs;
  1131. auto parts = va.splitter('.').array;
  1132. assert(parts.length == 3, "Version string with a digit group count != 3: "~va);
  1133.  
  1134. foreach (i; 0 .. 3) {
  1135. auto vp = parts[0 .. i+1].join(".");
  1136. auto ve = Version(expandVersion(vp));
  1137. auto veb = Version(bumpVersion(vp) ~ "-0");
  1138. if (ve == m_versA && veb == m_versB) return "~>" ~ vp;
  1139.  
  1140. auto veb2 = Version(bumpIncompatibleVersion(expandVersion(vp)) ~ "-0");
  1141. if (ve == m_versA && veb2 == m_versB) return "^" ~ vp;
  1142. }
  1143. }
  1144.  
  1145. if (m_versA != Version.minRelease) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString();
  1146. if (m_versB != Version.maxRelease) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString();
  1147. if (this.matchesAny()) r = ">=0.0.0";
  1148. return r;
  1149. }
  1150.  
  1151. public bool isValid() const @safe {
  1152. return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB);
  1153. }
  1154.  
  1155. private static bool doCmp(bool inclusive, in Version a, in Version b)
  1156. @safe
  1157. {
  1158. return inclusive ? a <= b : a < b;
  1159. }
  1160.  
  1161. private static bool isDigit(char ch) @safe { return ch >= '0' && ch <= '9'; }
  1162. private static string skipComp(ref string c) @safe {
  1163. size_t idx = 0;
  1164. while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.branchPrefix) idx++;
  1165. enforce(idx < c.length, "Expected version number in version spec: "~c);
  1166. string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx];
  1167. c = c[idx..$];
  1168. switch(cmp) {
  1169. default: enforce(false, "No/Unknown comparison specified: '"~cmp~"'"); return ">=";
  1170. case ">=": goto case; case ">": goto case;
  1171. case "<=": goto case; case "<": goto case;
  1172. case "==": return cmp;
  1173. }
  1174. }
  1175. }
  1176.  
  1177. enum VersionMatchMode {
  1178. standard, /// Match according to SemVer rules
  1179. strict /// Also include build metadata suffix in the comparison
  1180. }
  1181.  
  1182. unittest {
  1183. Version a, b;
  1184.  
  1185. assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed");
  1186. assert(!a.isBranch, "Error: '1.0.0' treated as branch");
  1187. assert(a == a, "a == a failed");
  1188.  
  1189. assertNotThrown(a = Version(Version.masterString), "Constructing Version("~Version.masterString~"') failed");
  1190. assert(a.isBranch, "Error: '"~Version.masterString~"' treated as branch");
  1191. assert(a.isMaster);
  1192. assert(a == Version.masterBranch, "Constructed master version != default master version.");
  1193.  
  1194. assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed.");
  1195. assert(a.isBranch, "Error: '~BRANCH' not treated as branch'");
  1196. assert(!a.isMaster);
  1197. assert(a == a, "a == a with branch failed");
  1198.  
  1199. // opCmp
  1200. a = Version("1.0.0");
  1201. b = Version("1.0.0");
  1202. assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed");
  1203. b = Version("2.0.0");
  1204. assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed");
  1205.  
  1206. a = Version.masterBranch;
  1207. b = Version("~BRANCH");
  1208. assert(a != b, "a != b with a:MASTER, b:'~branch' failed");
  1209. assert(a > b);
  1210. assert(a < Version("0.0.0"));
  1211. assert(b < Version("0.0.0"));
  1212. assert(a > Version("~Z"));
  1213. assert(b < Version("~Z"));
  1214.  
  1215. // SemVer 2.0.0-rc.2
  1216. a = Version("2.0.0-rc.2");
  1217. b = Version("2.0.0-rc.3");
  1218. assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3");
  1219.  
  1220. a = Version("2.0.0-rc.2+build-metadata");
  1221. b = Version("2.0.0+build-metadata");
  1222. assert(a < b, "Failed: "~a.toString()~"<"~b.toString());
  1223.  
  1224. // 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
  1225. Version[] versions;
  1226. versions ~= Version("1.0.0-alpha");
  1227. versions ~= Version("1.0.0-alpha.1");
  1228. versions ~= Version("1.0.0-beta.2");
  1229. versions ~= Version("1.0.0-beta.11");
  1230. versions ~= Version("1.0.0-rc.1");
  1231. versions ~= Version("1.0.0");
  1232. for(int i=1; i<versions.length; ++i)
  1233. for(int j=i-1; j>=0; --j)
  1234. assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString());
  1235.  
  1236. assert(Version("1.0.0+a") == Version("1.0.0+b"));
  1237.  
  1238. assert(Version("1.0.0").matches(Version("1.0.0+foo")));
  1239. assert(Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.standard));
  1240. assert(!Version("1.0.0").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
  1241. assert(Version("1.0.0+foo").matches(Version("1.0.0+foo"), VersionMatchMode.strict));
  1242. }
  1243.  
  1244. /// Determines whether the given string is a Git hash.
  1245. bool isGitHash(string hash) @nogc nothrow pure @safe
  1246. {
  1247. import std.ascii : isHexDigit;
  1248. import std.utf : byCodeUnit;
  1249.  
  1250. return hash.length >= 7 && hash.length <= 40 && hash.byCodeUnit.all!isHexDigit;
  1251. }
  1252.  
  1253. @nogc nothrow pure @safe unittest {
  1254. assert(isGitHash("73535568b79a0b124bc1653002637a830ce0fcb8"));
  1255. assert(!isGitHash("735"));
  1256. assert(!isGitHash("73535568b79a0b124bc1-53002637a830ce0fcb8"));
  1257. assert(!isGitHash("73535568b79a0b124bg1"));
  1258. }