diff --git a/build-files.txt b/build-files.txt index fbda121..22b1042 100644 --- a/build-files.txt +++ b/build-files.txt @@ -4,6 +4,7 @@ source/dub/dependencyresolver.d source/dub/description.d source/dub/dub.d +source/dub/git.d source/dub/init.d source/dub/packagemanager.d source/dub/packagesupplier.d diff --git a/changelog/submodules.dd b/changelog/submodules.dd new file mode 100644 index 0000000..50d2665 --- /dev/null +++ b/changelog/submodules.dd @@ -0,0 +1,5 @@ +Support for git submodules as packages + +Dub now supports the flag `--submodules`, which will scan the root folder for git submodules and add them as +available packages. +The git tag on the repository defines the version of the package. diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 597a10e..ef72045 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -11,6 +11,7 @@ import dub.dependency; import dub.dub; import dub.generators.generator; +import dub.git; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.data.json; @@ -266,6 +267,10 @@ // parent package. try dub.packageManager.getOrLoadPackage(NativePath(options.root_path)); catch (Exception e) { logDiagnostic("No valid package found in current working directory: %s", e.msg); } + + if (options.submodules) { + addGitSubmodules(dub.packageManager, NativePath(options.root_path)); + } } } @@ -284,12 +289,11 @@ } } - /** Contains and parses options common to all commands. */ struct CommonOptions { bool verbose, vverbose, quiet, vquiet, verror; - bool help, annotate, bare; + bool help, annotate, bare, submodules; string[] registry_urls; string root_path; SkipPackageSuppliers skipRegistry = SkipPackageSuppliers.none; @@ -314,6 +318,7 @@ ]); args.getopt("annotate", &annotate, ["Do not perform any action, just print what would be done"]); args.getopt("bare", &bare, ["Read only packages contained in the current directory"]); + args.getopt("submodules", &submodules, ["Include git submodules as direct packages"]); args.getopt("v|verbose", &verbose, ["Print diagnostic output"]); args.getopt("vverbose", &vverbose, ["Print debug output"]); args.getopt("q|quiet", &quiet, ["Only print warnings and errors"]); diff --git a/source/dub/git.d b/source/dub/git.d new file mode 100644 index 0000000..d6f18a1 --- /dev/null +++ b/source/dub/git.d @@ -0,0 +1,47 @@ +// functionality to supply packages from git submodules +module dub.git; + +import dub.dependency; +import dub.internal.vibecompat.core.file; +import dub.package_; +import dub.packagemanager; + +import std.algorithm; +import std.ascii : newline; +import std.exception : enforce; +import std.range; +import std.string; + +/** Adds the git submodules checked out in the root path as direct packages. + Package version is derived from the submodule's tag by `getOrLoadPackage`. + + Params: + packageManager = Package manager to track the added packages. + rootPath = the root path of the git repository to check for submodules. + */ +public void addGitSubmodules(PackageManager packageManager, NativePath rootPath) { + import std.process : execute; + + auto rootScmPath = rootPath ~ ".git"; + const submoduleInfo = execute([ + "git", + "-C", rootPath.toNativeString, + "--git-dir=" ~ (rootScmPath.relativeTo(rootPath)).toNativeString, + "submodule", "status"]); + + enforce(submoduleInfo.status == 0, + format("git submodule status exited with error code %s: %s", submoduleInfo.status, submoduleInfo.output)); + + foreach (line; submoduleInfo.output.lines) { + const parts = line.split(" ").map!strip.filter!(a => !a.empty).array; + const subPath = rootPath ~ parts[1]; + const packageFile = Package.findPackageFile(subPath); + + if (packageFile != NativePath.init) { + const scmPath = rootPath ~ NativePath(".git/modules/" ~ parts[1]); + packageManager.getOrLoadPackage(subPath, packageFile, false, scmPath); + } + } +} + +private alias lines = text => text.split(newline).map!strip.filter!(a => !a.empty); diff --git a/source/dub/package_.d b/source/dub/package_.d index 88e3765..28faac4 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -75,20 +75,24 @@ root = The directory in which the package resides (if any). parent = Reference to the parent package, if the new package is a sub package. + scm_path = The directory in which the VCS (Git) stores its state. + Different than root/.git for submodules. version_override = Optional version to associate to the package instead of the one declared in the package recipe, or the one - determined by invoking the VCS (GIT currently). + determined by invoking the VCS (Git currently). */ - this(Json json_recipe, NativePath root = NativePath(), Package parent = null, string version_override = "") + this(Json json_recipe, NativePath root = NativePath(), Package parent = null, + NativePath scm_path = NativePath(), string version_override = "") { import dub.recipe.json; PackageRecipe recipe; parseJson(recipe, json_recipe, parent ? parent.name : null); - this(recipe, root, parent, version_override); + this(recipe, root, parent, version_override, scm_path); } /// ditto - this(PackageRecipe recipe, NativePath root = NativePath(), Package parent = null, string version_override = "") + this(PackageRecipe recipe, NativePath root = NativePath(), Package parent = null, + string version_override = "", NativePath scm_path = NativePath()) { // save the original recipe m_rawRecipe = recipe.clone; @@ -98,15 +102,15 @@ // try to run git to determine the version of the package if no explicit version was given if (recipe.version_.length == 0 && !parent) { - try recipe.version_ = determineVersionFromSCM(root); + try recipe.version_ = determineVersionFromSCM(root, scm_path); catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg); if (recipe.version_.length == 0) { - logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString()); + logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, root.toNativeString()); // TODO: Assume unknown version here? // recipe.version_ = Version.unknown.toString(); recipe.version_ = Version.masterBranch.toString(); - } else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_); + } else logDiagnostic("Determined package version using Git: %s %s", recipe.name, recipe.version_); } m_parentPackage = parent; @@ -146,11 +150,15 @@ empty, the `root` directory will be searched for a recipe file. parent = Reference to the parent package, if the new package is a sub package. + scm_path = The directory in which the VCS (Git) stores its state. + Different than root/.git for submodules! version_override = Optional version to associate to the package instead of the one declared in the package recipe, or the one - determined by invoking the VCS (GIT currently). + determined by invoking the VCS (Git currently). */ - static Package load(NativePath root, NativePath recipe_file = NativePath.init, Package parent = null, string version_override = "") + static Package load(NativePath root, + NativePath recipe_file = NativePath.init, Package parent = null, + string version_override = "", NativePath scm_path = NativePath.init) { import dub.recipe.io; @@ -163,7 +171,7 @@ auto recipe = readPackageRecipe(recipe_file, parent ? parent.name : null); - auto ret = new Package(recipe, root, parent, version_override); + auto ret = new Package(recipe, root, parent, version_override, scm_path); ret.m_infoFile = recipe_file; return ret; } @@ -742,21 +750,24 @@ } } -private string determineVersionFromSCM(NativePath path) +private string determineVersionFromSCM(NativePath path, NativePath scm_path) { + if (scm_path.empty) { + scm_path = path ~ ".git"; + } // On Windows, which is slow at running external processes, // cache the version numbers that are determined using - // GIT to speed up the initialization phase. + // Git to speed up the initialization phase. version (Windows) { import std.file : exists, readText; - // quickly determine head commit without invoking GIT + // quickly determine head commit without invoking Git string head_commit; - auto hpath = (path ~ ".git/HEAD").toNativeString(); + auto hpath = (scm_path ~ "HEAD").toNativeString(); if (exists(hpath)) { auto head_ref = readText(hpath).strip(); if (head_ref.startsWith("ref: ")) { - auto rpath = (path ~ (".git/"~head_ref[5 .. $])).toNativeString(); + auto rpath = (scm_path ~ head_ref[5 .. $]).toNativeString(); if (exists(rpath)) head_commit = readText(rpath).strip(); } @@ -775,7 +786,7 @@ } // if no cache file or the HEAD commit changed, perform full detection - auto ret = determineVersionWithGIT(path); + auto ret = determineVersionWithGit(path, scm_path); version (Windows) { // update version cache file @@ -788,16 +799,19 @@ return ret; } -// determines the version of a package that is stored in a GIT working copy +// determines the version of a package that is stored in a Git working copy // by invoking the "git" executable -private string determineVersionWithGIT(NativePath path) +private string determineVersionWithGit(NativePath path, NativePath git_dir) { - import std.process; import dub.semver; + import std.process; + import std.typecons : tuple; - auto git_dir = path ~ ".git"; if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null; - auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString(); + auto git_dir_params = tuple( + "-C", path.toNativeString(), + "--git-dir=" ~ git_dir.relativeTo(path).toNativeString(), + ).expand; static string exec(scope string[] params...) { auto ret = executeShell(escapeShellCommand(params)); @@ -806,7 +820,7 @@ return null; } - auto tag = exec("git", git_dir_param, "describe", "--long", "--tags"); + auto tag = exec("git", git_dir_params, "describe", "--long", "--tags"); if (tag !is null) { auto parts = tag.split("-"); auto commit = parts[$-1]; @@ -819,7 +833,7 @@ } } - auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD"); + auto branch = exec("git", git_dir_params, "rev-parse", "--abbrev-ref", "HEAD"); if (branch !is null) { if (branch != "HEAD") return "~" ~ branch; } diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 13134b5..3664b52 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -187,18 +187,20 @@ Params: path = NativePath to the root directory of the package recipe_path = Optional path to the recipe file of the package + scm_path = The directory in which the VCS (Git) stores its state. allow_sub_packages = Also return a sub package if it resides in the given folder Returns: The packages loaded from the given path Throws: Throws an exception if no package can be loaded */ - Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init, bool allow_sub_packages = false) + Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init, + bool allow_sub_packages = false, NativePath scm_path = NativePath.init) { path.endsWithSlash = true; foreach (p; getPackageIterator()) if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path))) return p; - auto pack = Package.load(path, recipe_path); + auto pack = Package.load(path, recipe_path, null, "", scm_path); addPackages(m_temporaryPackages, pack); return pack; } diff --git a/test/git-submodule.sh b/test/git-submodule.sh new file mode 100755 index 0000000..e8b9b1b --- /dev/null +++ b/test/git-submodule.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -e + +. $(dirname "${BASH_SOURCE[0]}")/common.sh + +LAST_DIR=$PWD +TEMP_DIR="submodule-test" + +function cleanup { + cd "$LAST_DIR" + rm -rf "$TEMP_DIR" +} +trap cleanup EXIT + +mkdir "$TEMP_DIR" +cd "$TEMP_DIR" + +mkdir -p dependency/src + +cat << EOF >> dependency/dub.sdl +name "dependency" +sourcePaths "src" +EOF + +cat << EOF >> dependency/src/foo.d +module foo; +void foo() { } +EOF + +function git_ { + git -C dependency -c "user.name=Name" -c "user.email=Email" "$@" +} +git_ init +git_ add dub.sdl +git_ add src/foo.d +git_ commit -m "first commit" +git_ tag v1.0.0 + +mkdir project + +cat << EOF >> project/dub.sdl +name "project" +mainSourceFile "project.d" +targetType "executable" +dependency "dependency" version="1.0.0" +EOF + +cat << EOF >> project/project.d +module project; +import foo : foo; +void main() { foo(); } +EOF + +function git_ { + git -C project -c "user.name=Name" -c "user.email=Email" "$@" +} +git_ init +git_ add dub.sdl +git_ add project.d +git_ submodule add ../dependency dependency +git_ commit -m "first commit" + +# dub should now pick up the dependency +$DUB --root=project --submodules run + +if ! grep -c -e "\"dependency\": \"1.0.0\"" project/dub.selections.json; then + die $LINENO "Dependency version was not identified correctly." +fi