Newer
Older
dub_jkp / source / dub / package_.d
@Sönke Ludwig Sönke Ludwig on 10 Jul 2015 20 KB Fix naming convention of FileMode enum.
  1. /**
  2. Stuff with dependencies.
  3.  
  4. Copyright: © 2012-2013 Matthias Dondorff, © 2012-2015 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
  7. */
  8. module dub.package_;
  9.  
  10. public import dub.recipe.packagerecipe;
  11.  
  12. import dub.compilers.compiler;
  13. import dub.dependency;
  14. import dub.description;
  15. import dub.recipe.json;
  16. import dub.recipe.sdl;
  17.  
  18. import dub.internal.utils;
  19. import dub.internal.vibecompat.core.log;
  20. import dub.internal.vibecompat.core.file;
  21. import dub.internal.vibecompat.data.json;
  22. import dub.internal.vibecompat.inet.url;
  23.  
  24. import std.algorithm;
  25. import std.array;
  26. import std.conv;
  27. import std.exception;
  28. import std.file;
  29. import std.range;
  30. import std.string;
  31. import std.typecons : Nullable;
  32.  
  33.  
  34. enum PackageFormat {
  35. json,
  36. sdl
  37. }
  38.  
  39. struct FilenameAndFormat {
  40. string filename;
  41. PackageFormat format;
  42. }
  43.  
  44. struct PathAndFormat {
  45. Path path;
  46. PackageFormat format;
  47.  
  48. @property bool empty() { return path.empty; }
  49.  
  50. string toString() const { return path.toString(); }
  51. }
  52.  
  53.  
  54. // Supported package descriptions in decreasing order of preference.
  55. static immutable FilenameAndFormat[] packageInfoFiles = [
  56. {"dub.json", PackageFormat.json},
  57. {"dub.sdl", PackageFormat.sdl},
  58. {"package.json", PackageFormat.json}
  59. ];
  60.  
  61. @property string[] packageInfoFilenames() { return packageInfoFiles.map!(f => cast(string)f.filename).array; }
  62.  
  63. @property string defaultPackageFilename() { return packageInfoFiles[0].filename; }
  64.  
  65.  
  66. /**
  67. Represents a package, including its sub packages
  68.  
  69. Documentation of the dub.json can be found at
  70. http://registry.vibed.org/package-format
  71. */
  72. class Package {
  73. private {
  74. Path m_path;
  75. PathAndFormat m_infoFile;
  76. PackageRecipe m_info;
  77. Package m_parentPackage;
  78. }
  79.  
  80. static PathAndFormat findPackageFile(Path path)
  81. {
  82. foreach(file; packageInfoFiles) {
  83. auto filename = path ~ file.filename;
  84. if(existsFile(filename)) return PathAndFormat(filename, file.format);
  85. }
  86. return PathAndFormat(Path());
  87. }
  88.  
  89. this(Path root, PathAndFormat infoFile = PathAndFormat(), Package parent = null, string versionOverride = "")
  90. {
  91. RawPackage raw_package;
  92. m_infoFile = infoFile;
  93.  
  94. try {
  95. if(m_infoFile.empty) {
  96. m_infoFile = findPackageFile(root);
  97. if(m_infoFile.empty)
  98. throw new Exception(
  99. "No package file found in %s, expected one of %s"
  100. .format(root.toNativeString(), packageInfoFiles.map!(f => cast(string)f.filename).join("/")));
  101. }
  102. raw_package = rawPackageFromFile(m_infoFile);
  103. } catch (Exception ex) throw ex;//throw new Exception(format("Failed to load package %s: %s", m_infoFile.toNativeString(), ex.msg));
  104.  
  105. enforce(raw_package !is null, format("Missing package description for package at %s", root.toNativeString()));
  106. this(raw_package, root, parent, versionOverride);
  107. }
  108.  
  109. this(Json package_info, Path root = Path(), Package parent = null, string versionOverride = "")
  110. {
  111. this(new JsonPackage(package_info), root, parent, versionOverride);
  112. }
  113.  
  114. this(RawPackage raw_package, Path root = Path(), Package parent = null, string versionOverride = "")
  115. {
  116. PackageRecipe recipe;
  117.  
  118. // parse the Package description
  119. if(raw_package !is null)
  120. {
  121. scope(failure) logError("Failed to parse package description for %s %s in %s.",
  122. raw_package.package_name, versionOverride.length ? versionOverride : raw_package.version_,
  123. root.length ? root.toNativeString() : "remote location");
  124. raw_package.parseInto(recipe, parent ? parent.name : null);
  125.  
  126. if (!versionOverride.empty)
  127. recipe.version_ = versionOverride;
  128.  
  129. // try to run git to determine the version of the package if no explicit version was given
  130. if (recipe.version_.length == 0 && !parent) {
  131. try recipe.version_ = determineVersionFromSCM(root);
  132. catch (Exception e) logDebug("Failed to determine version by SCM: %s", e.msg);
  133.  
  134. if (recipe.version_.length == 0) {
  135. logDiagnostic("Note: Failed to determine version of package %s at %s. Assuming ~master.", recipe.name, this.path.toNativeString());
  136. // TODO: Assume unknown version here?
  137. // recipe.version_ = Version.UNKNOWN.toString();
  138. recipe.version_ = Version.MASTER.toString();
  139. } else logDiagnostic("Determined package version using GIT: %s %s", recipe.name, recipe.version_);
  140. }
  141. }
  142.  
  143. this(recipe, root, parent);
  144. }
  145.  
  146. this(PackageRecipe recipe, Path root = Path(), Package parent = null)
  147. {
  148. m_parentPackage = parent;
  149. m_path = root;
  150. m_path.endsWithSlash = true;
  151.  
  152. // use the given recipe as the basis
  153. m_info = recipe;
  154.  
  155. fillWithDefaults();
  156. simpleLint();
  157. }
  158.  
  159. @property string name()
  160. const {
  161. if (m_parentPackage) return m_parentPackage.name ~ ":" ~ m_info.name;
  162. else return m_info.name;
  163. }
  164. @property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; }
  165. @property Version ver() const { return Version(this.vers); }
  166. @property void ver(Version ver) { assert(m_parentPackage is null); m_info.version_ = ver.toString(); }
  167. @property ref inout(PackageRecipe) info() inout { return m_info; }
  168. @property Path path() const { return m_path; }
  169. @property Path packageInfoFilename() const { return m_infoFile.path; }
  170. @property const(Dependency[string]) dependencies() const { return m_info.dependencies; }
  171. @property inout(Package) basePackage() inout { return m_parentPackage ? m_parentPackage.basePackage : this; }
  172. @property inout(Package) parentPackage() inout { return m_parentPackage; }
  173. @property inout(SubPackage)[] subPackages() inout { return m_info.subPackages; }
  174.  
  175. @property string[] configurations()
  176. const {
  177. auto ret = appender!(string[])();
  178. foreach( ref config; m_info.configurations )
  179. ret.put(config.name);
  180. return ret.data;
  181. }
  182.  
  183. const(Dependency[string]) getDependencies(string config)
  184. const {
  185. Dependency[string] ret;
  186. foreach (k, v; m_info.buildSettings.dependencies)
  187. ret[k] = v;
  188. foreach (ref conf; m_info.configurations)
  189. if (conf.name == config) {
  190. foreach (k, v; conf.buildSettings.dependencies)
  191. ret[k] = v;
  192. break;
  193. }
  194. return ret;
  195. }
  196.  
  197. /** Overwrites the packge description file using the default filename with the current information.
  198. */
  199. void storeInfo()
  200. {
  201. storeInfo(m_path);
  202. m_infoFile = PathAndFormat(m_path ~ defaultPackageFilename);
  203. }
  204. /// ditto
  205. void storeInfo(Path path)
  206. const {
  207. enforce(!ver.isUnknown, "Trying to store a package with an 'unknown' version, this is not supported.");
  208. auto filename = path ~ defaultPackageFilename;
  209. auto dstFile = openFile(filename.toNativeString(), FileMode.createTrunc);
  210. scope(exit) dstFile.close();
  211. dstFile.writePrettyJsonString(m_info.toJson());
  212. }
  213.  
  214. Nullable!PackageRecipe getInternalSubPackage(string name)
  215. {
  216. foreach (ref p; m_info.subPackages)
  217. if (p.path.empty && p.recipe.name == name)
  218. return Nullable!PackageRecipe(p.recipe);
  219. return Nullable!PackageRecipe();
  220. }
  221.  
  222. void warnOnSpecialCompilerFlags()
  223. {
  224. // warn about use of special flags
  225. m_info.buildSettings.warnOnSpecialCompilerFlags(m_info.name, null);
  226. foreach (ref config; m_info.configurations)
  227. config.buildSettings.warnOnSpecialCompilerFlags(m_info.name, config.name);
  228. }
  229.  
  230. const(BuildSettingsTemplate) getBuildSettings(string config = null)
  231. const {
  232. if (config.length) {
  233. foreach (ref conf; m_info.configurations)
  234. if (conf.name == config)
  235. return conf.buildSettings;
  236. assert(false, "Unknown configuration: "~config);
  237. } else {
  238. return m_info.buildSettings;
  239. }
  240. }
  241.  
  242. /// Returns all BuildSettings for the given platform and config.
  243. BuildSettings getBuildSettings(in BuildPlatform platform, string config)
  244. const {
  245. BuildSettings ret;
  246. m_info.buildSettings.getPlatformSettings(ret, platform, this.path);
  247. bool found = false;
  248. foreach(ref conf; m_info.configurations){
  249. if( conf.name != config ) continue;
  250. conf.buildSettings.getPlatformSettings(ret, platform, this.path);
  251. found = true;
  252. break;
  253. }
  254. assert(found || config is null, "Unknown configuration for "~m_info.name~": "~config);
  255.  
  256. // construct default target name based on package name
  257. if( ret.targetName.empty ) ret.targetName = this.name.replace(":", "_");
  258.  
  259. // special support for DMD style flags
  260. getCompiler("dmd").extractBuildOptions(ret);
  261.  
  262. return ret;
  263. }
  264.  
  265. /// Returns the combination of all build settings for all configurations and platforms
  266. BuildSettings getCombinedBuildSettings()
  267. const {
  268. BuildSettings ret;
  269. m_info.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
  270. foreach(ref conf; m_info.configurations)
  271. conf.buildSettings.getPlatformSettings(ret, BuildPlatform.any, this.path);
  272.  
  273. // construct default target name based on package name
  274. if (ret.targetName.empty) ret.targetName = this.name.replace(":", "_");
  275.  
  276. // special support for DMD style flags
  277. getCompiler("dmd").extractBuildOptions(ret);
  278.  
  279. return ret;
  280. }
  281.  
  282. void addBuildTypeSettings(ref BuildSettings settings, in BuildPlatform platform, string build_type)
  283. const {
  284. if (build_type == "$DFLAGS") {
  285. import std.process;
  286. string dflags = environment.get("DFLAGS");
  287. settings.addDFlags(dflags.split());
  288. return;
  289. }
  290.  
  291. if (auto pbt = build_type in m_info.buildTypes) {
  292. logDiagnostic("Using custom build type '%s'.", build_type);
  293. pbt.getPlatformSettings(settings, platform, this.path);
  294. } else {
  295. with(BuildOption) switch (build_type) {
  296. default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type));
  297. case "plain": break;
  298. case "debug": settings.addOptions(debugMode, debugInfo); break;
  299. case "release": settings.addOptions(releaseMode, optimize, inline); break;
  300. case "release-nobounds": settings.addOptions(releaseMode, optimize, inline, noBoundsCheck); break;
  301. case "unittest": settings.addOptions(unittests, debugMode, debugInfo); break;
  302. case "docs": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Dddocs"); break;
  303. case "ddox": settings.addOptions(syntaxOnly); settings.addDFlags("-c", "-Df__dummy.html", "-Xfdocs.json"); break;
  304. case "profile": settings.addOptions(profile, optimize, inline, debugInfo); break;
  305. case "cov": settings.addOptions(coverage, debugInfo); break;
  306. case "unittest-cov": settings.addOptions(unittests, coverage, debugMode, debugInfo); break;
  307. }
  308. }
  309. }
  310.  
  311. string getSubConfiguration(string config, in Package dependency, in BuildPlatform platform)
  312. const {
  313. bool found = false;
  314. foreach(ref c; m_info.configurations){
  315. if( c.name == config ){
  316. if( auto pv = dependency.name in c.buildSettings.subConfigurations ) return *pv;
  317. found = true;
  318. break;
  319. }
  320. }
  321. assert(found || config is null, "Invalid configuration \""~config~"\" for "~this.name);
  322. if( auto pv = dependency.name in m_info.buildSettings.subConfigurations ) return *pv;
  323. return null;
  324. }
  325.  
  326. /// Returns the default configuration to build for the given platform
  327. string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false)
  328. const {
  329. foreach (ref conf; m_info.configurations) {
  330. if (!conf.matchesPlatform(platform)) continue;
  331. if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue;
  332. return conf.name;
  333. }
  334. return null;
  335. }
  336.  
  337. /// Returns a list of configurations suitable for the given platform
  338. string[] getPlatformConfigurations(in BuildPlatform platform, bool is_main_package = false)
  339. const {
  340. auto ret = appender!(string[]);
  341. foreach(ref conf; m_info.configurations){
  342. if (!conf.matchesPlatform(platform)) continue;
  343. if (!is_main_package && conf.buildSettings.targetType == TargetType.executable) continue;
  344. ret ~= conf.name;
  345. }
  346. if (ret.data.length == 0) ret.put(null);
  347. return ret.data;
  348. }
  349.  
  350. /// Human readable information of this package and its dependencies.
  351. string generateInfoString() const {
  352. string s;
  353. s ~= m_info.name ~ ", version '" ~ m_info.version_ ~ "'";
  354. s ~= "\n Dependencies:";
  355. foreach(string p, ref const Dependency v; m_info.dependencies)
  356. s ~= "\n " ~ p ~ ", version '" ~ v.toString() ~ "'";
  357. return s;
  358. }
  359.  
  360. bool hasDependency(string depname, string config)
  361. const {
  362. if (depname in m_info.buildSettings.dependencies) return true;
  363. foreach (ref c; m_info.configurations)
  364. if ((config.empty || c.name == config) && depname in c.buildSettings.dependencies)
  365. return true;
  366. return false;
  367. }
  368.  
  369. /** Returns a description of the package for use in IDEs or build tools.
  370. */
  371. PackageDescription describe(BuildPlatform platform, string config)
  372. const {
  373. PackageDescription ret;
  374. ret.configuration = config;
  375. ret.path = m_path.toNativeString();
  376. ret.name = this.name;
  377. ret.version_ = this.ver;
  378. ret.description = m_info.description;
  379. ret.homepage = m_info.homepage;
  380. ret.authors = m_info.authors.dup;
  381. ret.copyright = m_info.copyright;
  382. ret.license = m_info.license;
  383. ret.dependencies = getDependencies(config).keys;
  384.  
  385. // save build settings
  386. BuildSettings bs = getBuildSettings(platform, config);
  387. BuildSettings allbs = getCombinedBuildSettings();
  388.  
  389. ret.targetType = bs.targetType;
  390. ret.targetPath = bs.targetPath;
  391. ret.targetName = bs.targetName;
  392. if (ret.targetType != TargetType.none)
  393. ret.targetFileName = getTargetFileName(bs, platform);
  394. ret.workingDirectory = bs.workingDirectory;
  395. ret.mainSourceFile = bs.mainSourceFile;
  396. ret.dflags = bs.dflags;
  397. ret.lflags = bs.lflags;
  398. ret.libs = bs.libs;
  399. ret.copyFiles = bs.copyFiles;
  400. ret.versions = bs.versions;
  401. ret.debugVersions = bs.debugVersions;
  402. ret.importPaths = bs.importPaths;
  403. ret.stringImportPaths = bs.stringImportPaths;
  404. ret.preGenerateCommands = bs.preGenerateCommands;
  405. ret.postGenerateCommands = bs.postGenerateCommands;
  406. ret.preBuildCommands = bs.preBuildCommands;
  407. ret.postBuildCommands = bs.postBuildCommands;
  408.  
  409. // prettify build requirements output
  410. for (int i = 1; i <= BuildRequirement.max; i <<= 1)
  411. if (bs.requirements & cast(BuildRequirement)i)
  412. ret.buildRequirements ~= cast(BuildRequirement)i;
  413.  
  414. // prettify options output
  415. for (int i = 1; i <= BuildOption.max; i <<= 1)
  416. if (bs.options & cast(BuildOption)i)
  417. ret.options ~= cast(BuildOption)i;
  418.  
  419. // collect all possible source files and determine their types
  420. SourceFileRole[string] sourceFileTypes;
  421. foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport;
  422. foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport;
  423. foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource;
  424. foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport;
  425. foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_;
  426. foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source;
  427. foreach (f; sourceFileTypes.byKey.array.sort()) {
  428. SourceFileDescription sf;
  429. sf.path = f;
  430. sf.type = sourceFileTypes[f];
  431. ret.files ~= sf;
  432. }
  433.  
  434. return ret;
  435. }
  436. // ditto
  437. deprecated void describe(ref Json dst, BuildPlatform platform, string config)
  438. {
  439. auto res = describe(platform, config);
  440. foreach (string key, value; res.serializeToJson())
  441. dst[key] = value;
  442. }
  443.  
  444. private void fillWithDefaults()
  445. {
  446. auto bs = &m_info.buildSettings;
  447.  
  448. // check for default string import folders
  449. if ("" !in bs.stringImportPaths) {
  450. foreach(defvf; ["views"]){
  451. if( existsFile(m_path ~ defvf) )
  452. bs.stringImportPaths[""] ~= defvf;
  453. }
  454. }
  455.  
  456. // check for default source folders
  457. immutable hasSP = ("" in bs.sourcePaths) !is null;
  458. immutable hasIP = ("" in bs.importPaths) !is null;
  459. if (!hasSP || !hasIP) {
  460. foreach (defsf; ["source/", "src/"]) {
  461. if (existsFile(m_path ~ defsf)) {
  462. if (!hasSP) bs.sourcePaths[""] ~= defsf;
  463. if (!hasIP) bs.importPaths[""] ~= defsf;
  464. }
  465. }
  466. }
  467.  
  468. // check for default app_main
  469. string app_main_file;
  470. auto pkg_name = m_info.name.length ? m_info.name : "unknown";
  471. foreach(sf; bs.sourcePaths.get("", null)){
  472. auto p = m_path ~ sf;
  473. if( !existsFile(p) ) continue;
  474. foreach(fil; ["app.d", "main.d", pkg_name ~ "/main.d", pkg_name ~ "/" ~ "app.d"]){
  475. if( existsFile(p ~ fil) ) {
  476. app_main_file = (Path(sf) ~ fil).toNativeString();
  477. break;
  478. }
  479. }
  480. }
  481.  
  482. // generate default configurations if none are defined
  483. if (m_info.configurations.length == 0) {
  484. if (bs.targetType == TargetType.executable) {
  485. BuildSettingsTemplate app_settings;
  486. app_settings.targetType = TargetType.executable;
  487. if (bs.mainSourceFile.empty) app_settings.mainSourceFile = app_main_file;
  488. m_info.configurations ~= ConfigurationInfo("application", app_settings);
  489. } else if (bs.targetType != TargetType.none) {
  490. BuildSettingsTemplate lib_settings;
  491. lib_settings.targetType = bs.targetType == TargetType.autodetect ? TargetType.library : bs.targetType;
  492.  
  493. if (bs.targetType == TargetType.autodetect) {
  494. if (app_main_file.length) {
  495. lib_settings.excludedSourceFiles[""] ~= app_main_file;
  496.  
  497. BuildSettingsTemplate app_settings;
  498. app_settings.targetType = TargetType.executable;
  499. app_settings.mainSourceFile = app_main_file;
  500. m_info.configurations ~= ConfigurationInfo("application", app_settings);
  501. }
  502. }
  503.  
  504. m_info.configurations ~= ConfigurationInfo("library", lib_settings);
  505. }
  506. }
  507. }
  508.  
  509. private void simpleLint() const {
  510. if (m_parentPackage) {
  511. if (m_parentPackage.path != path) {
  512. if (info.license.length && info.license != m_parentPackage.info.license)
  513. logWarn("License in subpackage %s is different than it's parent package, this is discouraged.", name);
  514. }
  515. }
  516. if (name.empty) logWarn("The package in %s has no name.", path);
  517. }
  518.  
  519. private static RawPackage rawPackageFromFile(PathAndFormat file, bool silent_fail = false) {
  520. if( silent_fail && !existsFile(file.path) ) return null;
  521.  
  522. string text;
  523.  
  524. {
  525. auto f = openFile(file.path.toNativeString(), FileMode.read);
  526. scope(exit) f.close();
  527. text = stripUTF8Bom(cast(string)f.readAll());
  528. }
  529.  
  530. final switch(file.format) {
  531. case PackageFormat.json:
  532. return new JsonPackage(parseJsonString(text, file.path.toNativeString()));
  533. case PackageFormat.sdl:
  534. return new SdlPackage(text, file.path.toNativeString());
  535. }
  536. }
  537.  
  538. static abstract class RawPackage
  539. {
  540. string package_name; // Should already be lower case
  541. string version_;
  542. abstract void parseInto(ref PackageRecipe package_, string parent_name);
  543. }
  544. private static class JsonPackage : RawPackage
  545. {
  546. Json json;
  547. this(Json json) {
  548. this.json = json;
  549.  
  550. string nameLower;
  551. if(json.type == Json.Type.string) {
  552. nameLower = json.get!string.toLower();
  553. this.json = nameLower;
  554. } else {
  555. nameLower = json.name.get!string.toLower();
  556. this.package_name = nameLower;
  557.  
  558. Json versionJson = json["version"];
  559. this.version_ = (versionJson.type == Json.Type.undefined) ? null : versionJson.get!string;
  560. }
  561.  
  562. this.package_name = nameLower;
  563. }
  564. override void parseInto(ref PackageRecipe recipe, string parent_name)
  565. {
  566. recipe.parseJson(json, parent_name);
  567. }
  568. }
  569.  
  570. private static class SdlPackage : RawPackage
  571. {
  572. import dub.internal.sdlang;
  573. Tag sdl;
  574.  
  575. this(string sdl_text, string filename)
  576. {
  577. this.sdl = parseSource(sdl_text, filename);
  578. foreach (t; this.sdl.tags) {
  579. switch (t.name) {
  580. default: break;
  581. case "name":
  582. this.package_name = t.values[0].get!string.toLower();
  583. break;
  584. case "version":
  585. this.version_ = t.values[0].get!string;
  586. break;
  587. }
  588. }
  589. }
  590.  
  591. override void parseInto(ref PackageRecipe recipe, string parent_name)
  592. {
  593. recipe.parseSDL(sdl, parent_name);
  594. }
  595. }
  596. }
  597.  
  598. private string determineVersionFromSCM(Path path)
  599. {
  600. import std.process;
  601. import dub.semver;
  602.  
  603. auto git_dir = path ~ ".git";
  604. if (!existsFile(git_dir) || !isDir(git_dir.toNativeString)) return null;
  605. auto git_dir_param = "--git-dir=" ~ git_dir.toNativeString();
  606.  
  607. static string exec(scope string[] params...) {
  608. auto ret = executeShell(escapeShellCommand(params));
  609. if (ret.status == 0) return ret.output.strip;
  610. logDebug("'%s' failed with exit code %s: %s", params.join(" "), ret.status, ret.output.strip);
  611. return null;
  612. }
  613.  
  614. auto tag = exec("git", git_dir_param, "describe", "--long", "--tags");
  615. if (tag !is null) {
  616. auto parts = tag.split("-");
  617. auto commit = parts[$-1];
  618. auto num = parts[$-2].to!int;
  619. tag = parts[0 .. $-2].join("-");
  620. if (tag.startsWith("v") && isValidVersion(tag[1 .. $])) {
  621. if (num == 0) return tag[1 .. $];
  622. else if (tag.canFind("+")) return format("%s.commit.%s.%s", tag[1 .. $], num, commit);
  623. else return format("%s+commit.%s.%s", tag[1 .. $], num, commit);
  624. }
  625. }
  626.  
  627. auto branch = exec("git", git_dir_param, "rev-parse", "--abbrev-ref", "HEAD");
  628. if (branch !is null) {
  629. if (branch != "HEAD") return "~" ~ branch;
  630. }
  631.  
  632. return null;
  633. }