diff --git a/NetRevisionTask/Api.cs b/NetRevisionTask/Api.cs index b109054..51b2b86 100644 --- a/NetRevisionTask/Api.cs +++ b/NetRevisionTask/Api.cs @@ -1,4 +1,6 @@ +using System; using System.IO; +using System.Reflection; namespace NetRevisionTask { @@ -7,19 +9,24 @@ namespace NetRevisionTask /// public class Api { + #region MSBuild API + public static string GetVersion( string projectDir = null, string requiredVcs = null, string revisionFormat = null, string tagMatch = "v[0-9]*", bool removeTagV = true, - string copyright = null) + string copyright = null, + string configurationName = null, + string errorOnModifiedRepoPattern = null) { if (string.IsNullOrEmpty(projectDir)) projectDir = Directory.GetCurrentDirectory(); var logger = new ConsoleLogger(); - var (success, _, informationalVersion, _) = Common.GetVersion(projectDir, requiredVcs, revisionFormat, tagMatch, removeTagV, copyright ?? "", logger, true); + var (success, _, informationalVersion, _) = Common.GetVersion(projectDir, requiredVcs, revisionFormat, tagMatch, removeTagV, copyright ?? "", logger, true, + configurationName, errorOnModifiedRepoPattern); if (!success) { return null; @@ -33,18 +40,172 @@ public static string GetShortVersion( string revisionFormat = null, string tagMatch = "v[0-9]*", bool removeTagV = true, - string copyright = null) + string copyright = null, + string configurationName = null, + string errorOnModifiedRepoPattern = null) { if (string.IsNullOrEmpty(projectDir)) projectDir = Directory.GetCurrentDirectory(); var logger = new ConsoleLogger(); - var (success, version, _, _) = Common.GetVersion(projectDir, requiredVcs, revisionFormat, tagMatch, removeTagV, copyright ?? "", logger, true); + var (success, version, _, _) = Common.GetVersion(projectDir, requiredVcs, revisionFormat, tagMatch, removeTagV, copyright ?? "", logger, true, + configurationName, errorOnModifiedRepoPattern); if (!success) { return null; } return version; } + + #endregion + + #region Interactive API + + /// + /// The instance that contains data about a revision of the project directory. + /// + public RevisionData RevisionData = null; + + /// + /// The instance of the logger used by the Interactive API. + /// + private ILogger logger = null; + + /// + /// The revision format template. + /// + private string revisionFormat = null; + + /// + /// The instance that resolves a revision format with placeholders to a revision ID from the + /// specified revision data. + /// + private RevisionFormatter revisionFormatter = null; + + /// + /// Create an instance of the Interactive API. + /// + /// + /// The project directory to process by the version control system. + /// + /// + /// The required , or null if any VCS is acceptable. + /// + /// The revision format template. + /// + /// The global pattern of tag names to match. If empty or "*", all tags are accepted. + /// + /// + /// The value indicating whether a leading "v" followed by a digit will be removed from the + /// tag name. + /// + /// The value of the build configuration name. + /// True if initialization was successful, false otherwise. + public Api( + ILogger logger = null, + string projectDir = null, + string requiredVcs = null, + string revisionFormat = null, + string tagMatch = "v[0-9]*", + bool removeTagV = true, + string configurationName = null) + { + // instantiate the console logger if no custom logger was provided + if (logger == null) + { + logger = new ConsoleLogger(); + } + this.logger = logger; + logger.Success(typeof(Api).GetTypeInfo().Assembly + .GetCustomAttribute().Title + " v" + + typeof(Api).GetTypeInfo().Assembly + .GetCustomAttribute() + .InformationalVersion); + logger.Trace($"Constructing {typeof(Api).FullName} instance"); + logger.Trace($"Assigned logger '{logger.GetType().FullName}'"); + + // analyze the working directory + RevisionData = Common.ProcessDirectory(projectDir, requiredVcs, tagMatch, logger); + if (!string.IsNullOrEmpty(requiredVcs) && RevisionData.VcsProvider == null) + { + string message = $"The required version control system '{requiredVcs}' is not " + + "available or not used in the project directory"; + logger.Error(message); + + throw new Exception(message); + } + + // initialize the revision format + if (string.IsNullOrEmpty(revisionFormat)) + { + revisionFormat = Common.GetRevisionFormat(projectDir, logger, true); + } + if (string.IsNullOrEmpty(revisionFormat)) + { + revisionFormat = RevisionData.GetDefaultRevisionFormat(logger); + } + this.revisionFormat = revisionFormat; + + // initialize the RevisionFormatter + revisionFormatter = new RevisionFormatter + { + RevisionData = RevisionData, + RemoveTagV = removeTagV, + BuildTime = DateTimeOffset.Now, + ConfigurationName = configurationName + }; + + // initialization successfully completed + logger.Trace($"Instantiation of {typeof(Api).FullName} successfully completed"); + } + + /// + /// Destroy the instance of the Interactive API. + /// + ~Api() + { + // initialization successfully completed + if (logger != null) + { + logger.Trace($"Destructing {typeof(Api).FullName} instance"); + } + } + + /// + /// Get the short version. + /// + /// The short version on success, null otherwise. + public string GetShortVersion() + { + return revisionFormatter.ResolveShort(revisionFormat); + } + + /// + /// Get the full (informational) version. + /// + /// The full (informational) version on success, null otherwise. + public string GetVersion() + { + return revisionFormatter.Resolve(revisionFormat); + } + + /// + /// Resolves placeholders in a revision format string using the current data. + /// + /// The revision format string to resolve. + /// The resolved revision string. + public string Resolve(string str) + { + if (string.IsNullOrEmpty(str)) + { + return str; + } + else + { + return revisionFormatter.Resolve(str); + } + } + + #endregion Interactive API } } diff --git a/NetRevisionTask/AssemblyInfoHelper.cs b/NetRevisionTask/AssemblyInfoHelper.cs index 9ea7573..7cfbce7 100644 --- a/NetRevisionTask/AssemblyInfoHelper.cs +++ b/NetRevisionTask/AssemblyInfoHelper.cs @@ -78,6 +78,7 @@ public AssemblyInfoHelper(string projectDir, bool throwOnMissingFile, ILogger lo /// Indicates whether only the last number is replaced by the revision number. /// Indicates whether the copyright year is replaced. /// Indicates whether the final informational version string is displayed. + /// Indicates whether the AssemblyMetadata attribute is processed. /// The name of the patched AssemblyInfo file. public string PatchFile( string patchedFileDir, @@ -87,7 +88,8 @@ public string PatchFile( bool informationalAttribute, bool revOnly, bool copyrightAttribute, - bool echo) + bool echo, + bool metadataAttribute) { logger?.Trace($@"Patching file ""{fileName}""..."); ReadFileLines(FullFileName); @@ -111,7 +113,7 @@ public string PatchFile( } // Process all lines in the file - ResolveAllLines(rf, simpleAttributes, informationalAttribute, revOnly, copyrightAttribute, echo); + ResolveAllLines(rf, simpleAttributes, informationalAttribute, revOnly, copyrightAttribute, echo, metadataAttribute); // Write all lines to the file string patchedFileName = Path.Combine(patchedFileDir, "Nrt" + Path.GetFileName(fileName)); @@ -250,7 +252,8 @@ private void WriteFileLines(string patchedFileName) /// Indicates whether only the last number is replaced by the revision number. /// Indicates whether the copyright year is replaced. /// Indicates whether the final informational version string is displayed. - private void ResolveAllLines(RevisionFormatter rf, bool simpleAttributes, bool informationalAttribute, bool revOnly, bool copyrightAttribute, bool echo) + /// Indicates whether the AssemblyMetadata attribute is processed. + private void ResolveAllLines(RevisionFormatter rf, bool simpleAttributes, bool informationalAttribute, bool revOnly, bool copyrightAttribute, bool echo, bool metadataAttribute) { // Preparing a truncated dotted-numeric version if we may need it string truncVersion = null; @@ -385,6 +388,23 @@ private void ResolveAllLines(RevisionFormatter rf, bool simpleAttributes, bool i logger?.Trace($@" Replaced ""{match.Groups[2].Value}"" with ""{copyrightText}""."); } } + + if (metadataAttribute) + { + // Replace the value part of AssemblyMetadata with the resolved string of what + // was already there. Format: [assembly: AssemblyMetadata("Key", "Value")] + match = Regex.Match( + lines[i], + @"^(\s*\" + attrStart + @"\s*assembly\s*:\s*AssemblyMetadata\s*\(\s*"")(.*?)(""\s*,\s*"")(.*?)(""\s*\)\s*\" + attrEnd + @".*)$", + RegexOptions.IgnoreCase); + if (match.Success) + { + string metadataText = rf.Resolve(match.Groups[4].Value); + lines[i] = match.Groups[1].Value + match.Groups[2].Value + match.Groups[3].Value + metadataText + match.Groups[5].Value; + logger?.Success("Found AssemblyMetadata attribute."); + logger?.Trace($@" Replaced [{match.Groups[2].Value}] => ""{match.Groups[4].Value}"" with ""{metadataText}""."); + } + } } } diff --git a/NetRevisionTask/Common.cs b/NetRevisionTask/Common.cs index dc0b961..55de667 100644 --- a/NetRevisionTask/Common.cs +++ b/NetRevisionTask/Common.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Text.RegularExpressions; using NetRevisionTask.VcsProviders; namespace NetRevisionTask @@ -8,7 +9,8 @@ namespace NetRevisionTask internal class Common { public static (bool Success, string Version, string InformationalVersion, string Copyright) - GetVersion(string projectDir, string requiredVcs, string revisionFormat, string tagMatch, bool removeTagV, string copyright, ILogger logger, bool warnOnMissing) + GetVersion(string projectDir, string requiredVcs, string revisionFormat, string tagMatch, bool removeTagV, string copyright, ILogger logger, bool warnOnMissing, + string configurationName, string configurationNameErrorPattern) { // Analyse working directory RevisionData data = ProcessDirectory(projectDir, requiredVcs, tagMatch, logger); @@ -17,6 +19,10 @@ public static (bool Success, string Version, string InformationalVersion, string logger.Error($@"The required version control system ""{requiredVcs}"" is not available or not used in the project directory."); return (false, null, null, null); } + if (TriggerErrorIfRepoModified(logger, data, configurationNameErrorPattern, configurationName)) + { + return (false, null, null, null); + } if (string.IsNullOrEmpty(revisionFormat)) { revisionFormat = GetRevisionFormat(projectDir, logger, warnOnMissing); @@ -26,7 +32,7 @@ public static (bool Success, string Version, string InformationalVersion, string revisionFormat = data.GetDefaultRevisionFormat(logger); } - var rf = new RevisionFormatter { RevisionData = data, RemoveTagV = removeTagV }; + var rf = new RevisionFormatter { RevisionData = data, RemoveTagV = removeTagV, BuildTime = DateTimeOffset.Now, ConfigurationName = configurationName }; try { return (true, rf.ResolveShort(revisionFormat), rf.Resolve(revisionFormat), rf.Resolve(copyright)); @@ -145,5 +151,30 @@ public static string GetRevisionFormat(string projectDir, ILogger logger, bool w } return revisionFormat; } + + /// + /// Determines if a modified repository that matches the given pattern triggers a build error. + /// + /// The data about the current revision of the source directory. + /// The match pattern that shall trigger the error. + /// The name of the build configuration. + /// True if an error shall be triggered, false otherwise. + public static bool TriggerErrorIfRepoModified(ILogger logger, RevisionData data, string cfgNameMatchPattern, string cfgName) + { + if (!string.IsNullOrEmpty(cfgNameMatchPattern) && !string.IsNullOrEmpty(cfgName)) + { + if (data.IsModified) + { + var match = Regex.Match(cfgName, $@"^{cfgNameMatchPattern}$", RegexOptions.IgnoreCase); + if (match.Success) + { + logger.Error($@"The ""{cfgName}"" configuration does not allow builds with a modified {data.VcsProvider.Name} repository."); + return true; + } + } + } + + return false; + } } } diff --git a/NetRevisionTask/ILogger.cs b/NetRevisionTask/ILogger.cs index 1c13b5b..0244216 100644 --- a/NetRevisionTask/ILogger.cs +++ b/NetRevisionTask/ILogger.cs @@ -3,7 +3,7 @@ /// /// An interface that is used for logging internal and error events. /// - internal interface ILogger + public interface ILogger { /// /// Logs a raw output line of an executed application. diff --git a/NetRevisionTask/RevisionData.cs b/NetRevisionTask/RevisionData.cs index 63cd303..7a1c243 100644 --- a/NetRevisionTask/RevisionData.cs +++ b/NetRevisionTask/RevisionData.cs @@ -7,14 +7,14 @@ namespace NetRevisionTask /// /// Contains data about a revision of the source directory. /// - internal class RevisionData + public class RevisionData { #region Management properties /// /// Gets or sets the VCS provider that provided the data in the current instance. /// - public IVcsProvider VcsProvider { get; set; } + internal IVcsProvider VcsProvider { get; set; } #endregion Management properties @@ -23,72 +23,72 @@ internal class RevisionData /// /// Gets or sets the commit hash of the currently checked out revision. /// - public string CommitHash { get; set; } + public string CommitHash { get; internal set; } /// /// Gets or sets the revision number of the currently checked out revision. /// - public int RevisionNumber { get; set; } + public int RevisionNumber { get; internal set; } /// /// Gets or sets the commit time of the currently checked out revision. /// - public DateTimeOffset CommitTime { get; set; } + public DateTimeOffset CommitTime { get; internal set; } /// /// Gets or sets the author time of the currently checked out revision. /// - public DateTimeOffset AuthorTime { get; set; } + public DateTimeOffset AuthorTime { get; internal set; } /// /// Gets or sets a value indicating whether the working copy is modified. /// - public bool IsModified { get; set; } + public bool IsModified { get; internal set; } /// /// Gets or sets a value indicating whether the working copy contains mixed revisions. /// - public bool IsMixed { get; set; } + public bool IsMixed { get; internal set; } /// /// Gets or sets the repository URL of the working directory. /// - public string RepositoryUrl { get; set; } + public string RepositoryUrl { get; internal set; } /// /// Gets or sets the committer name of the currently checked out revision. /// - public string CommitterName { get; set; } + public string CommitterName { get; internal set; } /// /// Gets or sets the committer e-mail address of the currently checked out revision. /// - public string CommitterEMail { get; set; } + public string CommitterEMail { get; internal set; } /// /// Gets or sets the author name of the currently checked out revision. /// - public string AuthorName { get; set; } + public string AuthorName { get; internal set; } /// /// Gets or sets the author e-mail address of the currently checked out revision. /// - public string AuthorEMail { get; set; } + public string AuthorEMail { get; internal set; } /// /// Gets or sets the branch currently checked out in the working directory. /// - public string Branch { get; set; } + public string Branch { get; internal set; } /// /// Gets or sets the name of the most recent matching tag. /// - public string Tag { get; set; } + public string Tag { get; internal set; } /// /// Gets or sets the number of commits since the most recent matching tag. /// - public int CommitsAfterTag { get; set; } + public int CommitsAfterTag { get; internal set; } #endregion Revision data properties @@ -97,7 +97,7 @@ internal class RevisionData /// /// Normalizes all data properties to prevent null values. /// - public void Normalize() + internal void Normalize() { if (CommitHash == null) CommitHash = ""; if (RepositoryUrl == null) RepositoryUrl = ""; @@ -117,10 +117,10 @@ public void Normalize() } /// - /// Dumps the revision data is debug output is enabled. + /// Dumps the revision data if debug output is enabled. /// /// A logger. - public void DumpData(ILogger logger) + internal void DumpData(ILogger logger) { logger.Trace("Revision data:"); logger.Trace(" AuthorEMail: " + AuthorEMail); @@ -144,7 +144,7 @@ public void DumpData(ILogger logger) /// /// A logger. /// The default revision format. - public string GetDefaultRevisionFormat(ILogger logger) + internal string GetDefaultRevisionFormat(ILogger logger) { if (!string.IsNullOrEmpty(CommitHash) && !Regex.IsMatch(CommitHash, "^0+$")) { diff --git a/NetRevisionTask/RevisionFormatter.cs b/NetRevisionTask/RevisionFormatter.cs index e74f364..2d4b220 100644 --- a/NetRevisionTask/RevisionFormatter.cs +++ b/NetRevisionTask/RevisionFormatter.cs @@ -11,7 +11,7 @@ internal class RevisionFormatter { #region Static data - private static readonly DateTimeOffset buildTime = DateTimeOffset.Now; + private static DateTimeOffset buildTime = DateTimeOffset.Now; /// /// Alphabet for the base-28 encoding. This uses the digits 0–9 and all characters a–z that @@ -50,9 +50,18 @@ internal class RevisionFormatter public bool RemoveTagV { get; set; } /// - /// Gets the build time. + /// Gets or sets the build time. /// - public DateTimeOffset BuildTime => buildTime; + public DateTimeOffset BuildTime + { + get => buildTime; + set => buildTime = value; + } + + /// + /// Gets the value of the build configuration name. + /// + public string ConfigurationName { get; set; } #endregion Data properties @@ -102,6 +111,11 @@ public string Resolve(string format) } string tagName = RevisionData.Tag; + if (string.IsNullOrEmpty(tagName)) + { + // default value when no tag exists in repository + tagName = "v0.0.0.0"; + } if (RemoveTagV && Regex.IsMatch(tagName, "^v[0-9]")) { tagName = tagName.Substring(1); @@ -142,6 +156,16 @@ public string Resolve(string format) format = format.Replace("{copyright}", copyright); format = Regex.Replace(format, @"\{copyright:([0-9]+?)-?\}", m => (m.Groups[1].Value != copyright ? m.Groups[1].Value + "-" : "") + copyright); + // Build Configuration + if (ConfigurationName == null) + { + ConfigurationName = string.Empty; + } + format = format.Replace("{bconf}", ConfigurationName); + format = format.Replace("{BCONF}", ConfigurationName.ToUpperInvariant()); + format = Regex.Replace(format, @"\{bconf:(.*?):(.+?)\}", m => !Regex.IsMatch(ConfigurationName, $@"^{m.Groups[2].Value}$", RegexOptions.IgnoreCase) ? m.Groups[1].Value + ConfigurationName : ""); + format = Regex.Replace(format, @"\{BCONF:(.*?):(.+?)\}", m => !Regex.IsMatch(ConfigurationName, $@"^{m.Groups[2].Value}$", RegexOptions.IgnoreCase) ? m.Groups[1].Value + ConfigurationName.ToUpperInvariant() : ""); + // Return revision ID return format; } diff --git a/NetRevisionTask/Tasks/PatchAssemblyInfo.cs b/NetRevisionTask/Tasks/PatchAssemblyInfo.cs index 154be36..082224a 100644 --- a/NetRevisionTask/Tasks/PatchAssemblyInfo.cs +++ b/NetRevisionTask/Tasks/PatchAssemblyInfo.cs @@ -93,6 +93,22 @@ public class PatchAssemblyInfo : MSBuildTask /// public bool ShowRevision { get; set; } + /// + /// Gets or sets a value indicating whether the AssemblyMetadata attribute is processed. + /// + public bool ResolveMetadata { get; set; } + + /// + /// Gets or sets the value of the build configuration name. + /// + public string ConfigurationName { get; set; } + + /// + /// Gets or sets the value of the build configuration RegEx pattern that triggers an error + /// on match if the repository is modified. + /// + public string ErrorOnModifiedRepoPattern { get; set; } + #endregion Properties #region Task output properties @@ -144,7 +160,14 @@ public override bool Execute() RevisionFormat = data.GetDefaultRevisionFormat(logger); } - var rf = new RevisionFormatter { RevisionData = data, RemoveTagV = RemoveTagV }; + // check whether a modified repository triggers a build error + if (Common.TriggerErrorIfRepoModified(logger, data, ErrorOnModifiedRepoPattern, ConfigurationName)) + { + return false; + } + + var rf = new RevisionFormatter { RevisionData = data, RemoveTagV = RemoveTagV, + ConfigurationName = ConfigurationName }; try { var aih = new AssemblyInfoHelper(ProjectDir, true, logger); @@ -157,7 +180,8 @@ public override bool Execute() ResolveInformationalAttribute, RevisionNumberOnly, ResolveCopyright, - ShowRevision); + ShowRevision, + ResolveMetadata); } catch (FormatException ex) { diff --git a/NetRevisionTask/Tasks/SetVersion.cs b/NetRevisionTask/Tasks/SetVersion.cs index ed450ac..806a8da 100644 --- a/NetRevisionTask/Tasks/SetVersion.cs +++ b/NetRevisionTask/Tasks/SetVersion.cs @@ -75,6 +75,17 @@ public class SetVersion : MSBuildTask /// public bool ShowRevision { get; set; } + /// + /// Gets the value of the build configuration name. + /// + public string ConfigurationName { get; set; } + + /// + /// Gets or sets the value of the build configuration RegEx pattern that triggers an error + /// on match if the repository is modified. + /// + public string ErrorOnModifiedRepoPattern { get; set; } + #endregion Properties #region Task output properties @@ -117,7 +128,8 @@ public override bool Execute() logger.Trace($"NetRevisionTask: SetVersion ({targetFramework})"); bool warnOnMissing = !GenerateAssemblyInfo && (NuGetPackOutput == null || NuGetPackOutput.Length == 0); - var result = Common.GetVersion(ProjectDir, RequiredVcs, RevisionFormat, TagMatch, RemoveTagV, Copyright ?? "", logger, warnOnMissing); + var result = Common.GetVersion(ProjectDir, RequiredVcs, RevisionFormat, TagMatch, RemoveTagV, Copyright ?? "", logger, warnOnMissing, + ConfigurationName, ErrorOnModifiedRepoPattern); if (!result.Success) { return false; diff --git a/NetRevisionTask/build/Unclassified.NetRevisionTask.targets b/NetRevisionTask/build/Unclassified.NetRevisionTask.targets index 6c0130c..38a20f7 100644 --- a/NetRevisionTask/build/Unclassified.NetRevisionTask.targets +++ b/NetRevisionTask/build/Unclassified.NetRevisionTask.targets @@ -13,6 +13,7 @@ true true true + true v[0-9]* true $(MSBuildProjectDirectory) @@ -37,7 +38,9 @@ RemoveTagV="$(NrtRemoveTagV)" ResolveCopyright="$(NrtResolveCopyright)" Copyright="$(Copyright)" - ShowRevision="$(NrtShowRevision)"> + ShowRevision="$(NrtShowRevision)" + ConfigurationName="$(ConfigurationName)" + ErrorOnModifiedRepoPattern="$(NrtErrorOnModifiedRepoPattern)"> @@ -72,7 +75,10 @@ ResolveInformationalAttribute="$(NrtResolveInformationalAttribute)" RevisionNumberOnly="$(NrtRevisionNumberOnly)" ResolveCopyright="$(NrtResolveCopyright)" - ShowRevision="$(NrtShowRevision)"> + ShowRevision="$(NrtShowRevision)" + ResolveMetadata="$(NrtResolveMetadata)" + ConfigurationName="$(ConfigurationName)" + ErrorOnModifiedRepoPattern="$(NrtErrorOnModifiedRepoPattern)"> diff --git a/README.md b/README.md index ed53277..e2d11da 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Example: git true $(MSBuildProjectDirectory) + true + .*Release.* The following MSBuild properties are supported: @@ -121,6 +123,14 @@ Specifies whether the determined revision ID is printed during the build with hi Sets the directory where NRT starts searching for the VCS files. This is helpful if NRT is added to a project that is a submodule of another repository and should observe the parent repository. +**NrtResolveMetadata**: boolean, default: true. + +Specifies whether the value component of the `AssemblyMetadata` (`AssemblyMetadataAttribute`) is resolved. + +**NrtErrorOnModifiedRepoPattern**: string, default: “”. + +Specifies a case-insensitive RegEx pattern string matching the build configuration string to trigger a build error if the repository contains modifications. If the string is empty, the functionality is disabled. + ### Revision format You can customise the format of the resulting version with a revision format string that defines how information about the commit or revision is formatted into the final revision ID. It is a plain string that contains placeholders in `{curly braces}`. Each placeholder is a simple data field or encodes a time value using a scheme and optional configuration arguments. @@ -183,6 +193,12 @@ The following data field placeholders are supported: **`{copyright:-}`**: Abbreviation for the copyright year range, starting at ``. The following dash is optional but recommended for clearer understanding. +**`{bconf}`**: Build configuration. + +**`{BCONF}`**: Build configuration, in upper case. + +**`{bconf::}`, `{BCONF::}`**: Build configuration, if not matching case-insensitive RegEx `` pattern, separated by ``, otherwise empty. + Schemes convert a commit or build time to a compact string representation. They can be used to assign incrementing versions if no revision number is provided by the VCS. First, select from the build, commit or authoring time with `{b:…}`, `{c:…}` or `{a:…}`. This is followed by the scheme name. There are 4 types of schemes. The following time schemes are supported: