diff --git a/source/dub/dependency.d b/source/dub/dependency.d index e1e40b0..14f3d52 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -34,7 +34,6 @@ Dependency spec; } - /** Represents a dependency specification. @@ -56,6 +55,7 @@ NativePath m_path; bool m_optional = false; bool m_default = false; + Repository m_repository; } /// A Dependency, which matches every valid version. @@ -93,11 +93,28 @@ m_path = path; } + this(Repository repository, string spec) { + this.versionSpec = spec; + this.repository = repository; + } + /// If set, overrides any version based dependency selection. @property void path(NativePath value) { m_path = value; } /// ditto @property NativePath path() const { return m_path; } + /// If set, overrides any version based dependency selection. + @property void repository(Repository value) + { + m_repository = value; + } + + /// ditto + @property Repository repository() const + { + return m_repository; + } + /// Determines if the dependency is required or optional. @property bool optional() const { return m_optional; } /// ditto @@ -111,6 +128,8 @@ /// Returns true $(I iff) the version range only matches a specific version. @property bool isExactVersion() const { return m_versA == m_versB; } + @property bool isGit() const { return !repository.empty; } + /// Returns the exact version matched by the version range. @property Version version_() const { enforce(m_versA == m_versB, "Dependency "~this.versionSpec~" is no exact version."); @@ -156,7 +175,7 @@ ves = ves[2..$]; m_versA = Version(expandVersion(ves)); m_versB = Version(bumpVersion(ves) ~ "-0"); - } else if (ves[0] == Version.branchPrefix) { + } else if (ves[0] == Version.branchPrefix || ves.isHash) { m_inclusiveA = true; m_inclusiveB = true; m_versA = m_versB = Version(ves); @@ -207,7 +226,6 @@ string r; if (this == invalid) return "invalid"; - if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { // Special "==" case if (m_versA == Version.masterBranch) return "~master"; @@ -258,7 +276,12 @@ */ string toString()() const { - auto ret = versionSpec; + string ret; + + if (!repository.empty) { + ret ~= "#"; + } + ret ~= versionSpec; if (optional) { if (default_) ret ~= " (optional, default)"; else ret ~= " (optional)"; @@ -288,6 +311,7 @@ json = Json.emptyObject; json["version"] = this.versionSpec; if (!path.empty) json["path"] = path.toString(); + if (!repository.empty) json["repository"] = repository.remote; if (optional) json["optional"] = true; if (default_) json["default"] = true; } @@ -316,6 +340,10 @@ dep = Dependency.any; dep.path = NativePath(verspec["path"].get!string); + } else if (auto repository = "repository" in verspec) { + enforce("version" in verspec, "No version field specified!"); + dep = Dependency(Repository(repository.get!string), + verspec["version"].get!string); } else { enforce("version" in verspec, "No version field specified!"); auto ver = verspec["version"].get!string; @@ -397,6 +425,7 @@ A specification is valid if it can match at least one version. */ bool valid() const { + if (isGit) return true; return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); } @@ -426,6 +455,7 @@ /// ditto bool matches(ref const(Version) v) const { if (this.matchesAny) return true; + if (this.isGit) return true; //logDebug(" try match: %s with: %s", v, this); // Master only matches master if(m_versA.isBranch) { @@ -449,6 +479,13 @@ */ Dependency merge(ref const(Dependency) o) const { + if (this.isGit) { + if (!o.isGit) return this; + if (this.m_versA == o.m_versB) return this; + return invalid; + } + if (o.isGit) return o; + if (this.matchesAny) return o; if (o.matchesAny) return this; if (m_versA.isBranch != o.m_versA.isBranch) return invalid; @@ -653,6 +690,34 @@ assert(Dependency("~>1.0.4+1.2.3").versionSpec == "~>1.0.4"); } +/** + Represents an SCM repository. +*/ +struct Repository +{ + private string m_remote; + + /** + Returns: + Repository URL or path. + */ + @property string remote() @nogc nothrow pure @safe + in { assert(m_remote !is null); } + body + { + return m_remote; + } + + /** + Returns: + Whether the repository was initialized with an URL or path. + */ + @property bool empty() const @nogc nothrow pure @safe + { + return m_remote is null; + } +} + /** Represents a version in semantic version format, or a branch identifier. @@ -681,7 +746,7 @@ this(string vers) { enforce(vers.length > 1, "Version strings must not be empty."); - if (vers[0] != branchPrefix && vers.ptr !is UNKNOWN_VERS.ptr) + if (vers[0] != branchPrefix && !vers.isHash && vers.ptr !is UNKNOWN_VERS.ptr) enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers); m_version = vers; } @@ -695,6 +760,9 @@ bool opEquals(const Version oth) const { return opCmp(oth) == 0; } + /// Tests if this represents a hash instead of a version. + @property bool isGit() const { return m_version.isHash; } + /// Tests if this represents a branch instead of a version. @property bool isBranch() const { return m_version.length > 0 && m_version[0] == branchPrefix; } @@ -706,7 +774,7 @@ Note that branches are always considered pre-release versions. */ @property bool isPreRelease() const { - if (isBranch) return true; + if (isBranch || isGit) return true; return isPreReleaseVersion(m_version); } @@ -725,6 +793,13 @@ if (isUnknown || other.isUnknown) { throw new Exception("Can't compare unknown versions! (this: %s, other: %s)".format(this, other)); } + + if (isGit || other.isGit) { + if (!isGit) return -1; + if (!other.isGit) return 1; + return (m_version == m_version) ? 0 : 1; + } + if (isBranch || other.isBranch) { if(m_version == other.m_version) return 0; if (!isBranch) return 1; @@ -766,6 +841,7 @@ assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed"); b = Version("2.0.0"); assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed"); + a = Version.masterBranch; b = Version("~BRANCH"); assert(a != b, "a != b with a:MASTER, b:'~branch' failed"); @@ -805,4 +881,20 @@ assertThrown(a == b, "Failed: UNKNOWN == UNKNOWN"); assert(Version("1.0.0+a") == Version("1.0.0+b")); + + assert(Version("73535568b79a0b124bc1653002637a830ce0fcb8").isGit); +} + +private bool isHash(string hash) @nogc nothrow pure @safe +{ + import std.ascii : isAlphaNum; + import std.utf : byCodeUnit; + + return hash.length == 40 && hash.byCodeUnit.all!isAlphaNum; +} + +@nogc nothrow pure @safe unittest { + assert(isHash("73535568b79a0b124bc1653002637a830ce0fcb8")); + assert(!isHash("735")); + assert(!isHash("73535568b79a0b124bc1-53002637a830ce0fcb8")); } diff --git a/source/dub/dub.d b/source/dub/dub.d index 3f0d9ca..a7ed3d3 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -504,6 +504,9 @@ if (!path.absolute) path = this.rootPath ~ path; try if (m_packageManager.getOrLoadPackage(path)) continue; catch (Exception e) { logDebug("Failed to load path based selection: %s", e.toString().sanitize); } + } else if (!dep.repository.empty) { + if (m_packageManager.clonePackage(getBasePackageName(p), dep)) + continue; } else { if (m_packageManager.getPackage(p, dep.version_)) continue; foreach (ps; m_packageSuppliers) { @@ -534,7 +537,7 @@ string rootbasename = getBasePackageName(m_project.rootPackage.name); foreach (p, ver; versions) { - if (!ver.path.empty) continue; + if (!ver.path.empty || !ver.repository.empty) continue; auto basename = getBasePackageName(p); if (basename == rootbasename) continue; @@ -546,7 +549,7 @@ continue; } auto sver = m_project.selections.getSelectedVersion(basename); - if (!sver.path.empty) continue; + if (!sver.path.empty || !sver.repository.empty) continue; if (ver.version_ <= sver.version_) continue; logInfo("Package %s would be upgraded from %s to %s.", basename, sver, ver); @@ -566,6 +569,8 @@ logDebug("Failed to load path based selection: %s", e.toString().sanitize); continue; } + } else if (!ver.repository.empty) { + pack = m_packageManager.clonePackage(p, ver); } else { pack = m_packageManager.getBestPackage(p, ver); if (pack && m_packageManager.isManagedPackage(pack) @@ -582,7 +587,9 @@ fetchOpts |= (options & UpgradeOptions.preRelease) != 0 ? FetchOptions.usePrerelease : FetchOptions.none; if (!pack) fetch(p, ver, defaultPlacementLocation, fetchOpts, "getting selected version"); if ((options & UpgradeOptions.select) && p != m_project.rootPackage.name) { - if (ver.path.empty) m_project.selections.selectVersion(p, ver.version_); + if (!ver.repository.empty) { + m_project.selections.selectVersion(p, ver.repository, ver.versionSpec); + } else if (ver.path.empty) m_project.selections.selectVersion(p, ver.version_); else { NativePath relpath = ver.path; if (relpath.absolute) relpath = relpath.relativeTo(m_project.rootPackage.path); @@ -1580,6 +1587,8 @@ protected override Dependency[] getSpecificConfigs(string pack, TreeNodes nodes) { if (!nodes.configs.path.empty && getPackage(pack, nodes.configs)) return [nodes.configs]; + else if (!nodes.configs.repository.empty && getPackage(pack, nodes.configs)) + return [nodes.configs]; else return null; } @@ -1699,7 +1708,10 @@ if (basename == m_rootPackage.basePackage.name) return m_rootPackage.basePackage; - if (!dep.path.empty) { + if (!dep.repository.empty) { + auto ret = m_dub.packageManager.clonePackage(name, dep); + return ret !is null && dep.matches(ret.version_) ? ret : null; + } else if (!dep.path.empty) { try { auto ret = m_dub.packageManager.getOrLoadPackage(dep.path); if (dep.matches(ret.version_)) return ret; diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 13134b5..02c2dcb 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -203,6 +203,55 @@ return pack; } + /** For a given Git repository, returns the corresponding package. + + Git repository is provided as its remote URL, the repository is cloned + and in the dependency speicfied commit is checked out. + + If the target directory already exists, just returns the package + without cloning. + + Params: + name = Package name + dependency = Dependency that contains the repository URL and a specific commit + + Returns: + The package loaded from the given Git repository or null if the + package couldn't be loaded. + */ + Package clonePackage(string name, Dependency dependency) + in { assert(!dependency.repository.empty); } + body { + import std.process : escapeShellCommand, executeShell; + + string gitReference = dependency.versionSpec; + if (gitReference.startsWith("~")) gitReference.popFront; + + const destination = m_repositories[LocalPackageType.user].packagePath ~ + NativePath(name~"-"~gitReference) ~ name; + const nativeDestination = destination.toNativeString(); + + if (!exists(nativeDestination)) { + auto command = escapeShellCommand("git", "clone", + dependency.repository.remote, + nativeDestination); + if (executeShell(command).status != 0) { + return null; + } + + command = escapeShellCommand("git", + "-C", nativeDestination, + "checkout", gitReference); + if (executeShell(command).status != 0) { + rmdirRecurse(nativeDestination); + return null; + } + } + auto pack = Package.load(destination); + addPackages(m_temporaryPackages, pack); + + return pack; + } /** Searches for the latest version of a package matching the given dependency. */ diff --git a/source/dub/project.d b/source/dub/project.d index 8a807a9..f12734b 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -256,7 +256,7 @@ enforce(!m_rootPackage.name.canFind(' '), "Aborting due to the package name containing spaces."); foreach (d; m_rootPackage.getAllDependencies()) - if (d.spec.isExactVersion && d.spec.version_.isBranch) { + if (d.spec.isExactVersion && d.spec.version_.isBranch && !d.spec.isGit) { logWarn("WARNING: A deprecated branch based version specification is used " ~ "for the dependency %s. Please use numbered versions instead. Also " ~ "note that you can still use the %s file to override a certain " @@ -354,7 +354,10 @@ } } else if (m_selections.hasSelectedVersion(basename)) { vspec = m_selections.getSelectedVersion(basename); - if (vspec.path.empty) p = m_packageManager.getBestPackage(dep.name, vspec); + if (!vspec.repository.empty) { + p = m_packageManager.clonePackage(basename, vspec); + if (subname.length) p = m_packageManager.getSubPackage(p, subname, true); + } else if (vspec.path.empty) p = m_packageManager.getBestPackage(dep.name, vspec); else { auto path = vspec.path; if (!path.absolute) path = m_rootPackage.path ~ path; @@ -372,6 +375,11 @@ indent, basename, dep.name, pack.name); } + if (!p && !vspec.repository.empty) { + p = m_packageManager.clonePackage(basename, vspec); + if (subname.length) p = m_packageManager.getSubPackage(p, subname, false); + } + if (!p && !vspec.path.empty) { NativePath path = vspec.path; if (!path.absolute) path = pack.path ~ path; @@ -1542,6 +1550,17 @@ m_dirty = true; } + void selectVersion(string package_id, Repository repository, string spec) + { + const dependency = Dependency(repository, spec); + if (auto ps = package_id in m_selections) { + if (ps.dep == dependency) + return; + } + m_selections[package_id] = Selected(dependency); + m_dirty = true; + } + /// Removes the selection for a particular package. void deselectVersion(string package_id) { @@ -1604,7 +1623,12 @@ static Json dependencyToJson(Dependency d) { - if (d.path.empty) return Json(d.version_.toString()); + if (!d.repository.empty) { + return serializeToJson([ + "version": d.version_.toString(), + "repository": d.repository.remote, + ]); + } else if (d.path.empty) return Json(d.version_.toString()); else return serializeToJson(["path": d.path.toString()]); } @@ -1612,8 +1636,10 @@ { if (j.type == Json.Type.string) return Dependency(Version(j.get!string)); - else if (j.type == Json.Type.object) + else if (j.type == Json.Type.object && "path" in j) return Dependency(NativePath(j["path"].get!string)); + else if (j.type == Json.Type.object && "repository" in j) + return Dependency(Repository(j["repository"].get!string), j["version"].get!string); else throw new Exception(format("Unexpected type for dependency: %s", j.type)); }