Skip to content
Open
14 changes: 10 additions & 4 deletions NetRevisionTask/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ public static string GetVersion(
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;
Expand All @@ -33,13 +36,16 @@ 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;
Expand Down
26 changes: 23 additions & 3 deletions NetRevisionTask/AssemblyInfoHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public AssemblyInfoHelper(string projectDir, bool throwOnMissingFile, ILogger lo
/// <param name="revOnly">Indicates whether only the last number is replaced by the revision number.</param>
/// <param name="copyrightAttribute">Indicates whether the copyright year is replaced.</param>
/// <param name="echo">Indicates whether the final informational version string is displayed.</param>
/// <param name="metadataAttribute">Indicates whether the AssemblyMetadata attribute is processed.</param>
/// <returns>The name of the patched AssemblyInfo file.</returns>
public string PatchFile(
string patchedFileDir,
Expand All @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -250,7 +252,8 @@ private void WriteFileLines(string patchedFileName)
/// <param name="revOnly">Indicates whether only the last number is replaced by the revision number.</param>
/// <param name="copyrightAttribute">Indicates whether the copyright year is replaced.</param>
/// <param name="echo">Indicates whether the final informational version string is displayed.</param>
private void ResolveAllLines(RevisionFormatter rf, bool simpleAttributes, bool informationalAttribute, bool revOnly, bool copyrightAttribute, bool echo)
/// <param name="metadataAttribute">Indicates whether the AssemblyMetadata attribute is processed.</param>
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;
Expand Down Expand Up @@ -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}"".");
}
}
}
}

Expand Down
35 changes: 33 additions & 2 deletions NetRevisionTask/Common.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using NetRevisionTask.VcsProviders;

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);
Expand All @@ -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);
Expand All @@ -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));
Expand Down Expand Up @@ -145,5 +151,30 @@ public static string GetRevisionFormat(string projectDir, ILogger logger, bool w
}
return revisionFormat;
}

/// <summary>
/// Determines if a modified repository that matches the given pattern triggers a build error.
/// </summary>
/// <param name="data">The data about the current revision of the source directory.</param>
/// <param name="cfgNamePattern">The match pattern that shall trigger the error.</param>
/// <param name="cfgName">The name of the build configuration.</param>
/// <returns>True if an error shall be triggered, false otherwise.</returns>
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;
}
}
}
30 changes: 27 additions & 3 deletions NetRevisionTask/RevisionFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal class RevisionFormatter
{
#region Static data

private static readonly DateTimeOffset buildTime = DateTimeOffset.Now;
private static DateTimeOffset buildTime = DateTimeOffset.Now;

/// <summary>
/// Alphabet for the base-28 encoding. This uses the digits 0–9 and all characters a–z that
Expand Down Expand Up @@ -50,9 +50,18 @@ internal class RevisionFormatter
public bool RemoveTagV { get; set; }

/// <summary>
/// Gets the build time.
/// Gets or sets the build time.
/// </summary>
public DateTimeOffset BuildTime => buildTime;
public DateTimeOffset BuildTime
{
get => buildTime;
set => buildTime = value;
}

/// <summary>
/// Gets the value of the build configuration name.
/// </summary>
public string ConfigurationName { get; set; }

#endregion Data properties

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
28 changes: 26 additions & 2 deletions NetRevisionTask/Tasks/PatchAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ public class PatchAssemblyInfo : MSBuildTask
/// </summary>
public bool ShowRevision { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the AssemblyMetadata attribute is processed.
/// </summary>
public bool ResolveMetadata { get; set; }

/// <summary>
/// Gets or sets the value of the build configuration name.
/// </summary>
public string ConfigurationName { get; set; }

/// <summary>
/// Gets or sets the value of the build configuration RegEx pattern that triggers an error
/// on match if the repository is modified.
/// </summary>
public string ErrorOnModifiedRepoPattern { get; set; }

#endregion Properties

#region Task output properties
Expand Down Expand Up @@ -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);
Expand All @@ -157,7 +180,8 @@ public override bool Execute()
ResolveInformationalAttribute,
RevisionNumberOnly,
ResolveCopyright,
ShowRevision);
ShowRevision,
ResolveMetadata);
}
catch (FormatException ex)
{
Expand Down
14 changes: 13 additions & 1 deletion NetRevisionTask/Tasks/SetVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ public class SetVersion : MSBuildTask
/// </summary>
public bool ShowRevision { get; set; }

/// <summary>
/// Gets the value of the build configuration name.
/// </summary>
public string ConfigurationName { get; set; }

/// <summary>
/// Gets or sets the value of the build configuration RegEx pattern that triggers an error
/// on match if the repository is modified.
/// </summary>
public string ErrorOnModifiedRepoPattern { get; set; }

#endregion Properties

#region Task output properties
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions NetRevisionTask/build/Unclassified.NetRevisionTask.targets
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<NrtResolveSimpleAttributes Condition="'$(NrtResolveSimpleAttributes)' == ''">true</NrtResolveSimpleAttributes>
<NrtResolveInformationalAttribute Condition="'$(NrtResolveInformationalAttribute)' == ''">true</NrtResolveInformationalAttribute>
<NrtResolveCopyright Condition="'$(NrtResolveCopyright)' == ''">true</NrtResolveCopyright>
<NrtResolveMetadata Condition="'$(NrtResolveMetadata)' == ''">true</NrtResolveMetadata>
<NrtTagMatch Condition="'$(NrtTagMatch)' == ''">v[0-9]*</NrtTagMatch>
<NrtRemoveTagV Condition="'$(NrtRemoveTagV)' == ''">true</NrtRemoveTagV>
<NrtProjectDir Condition="'$(NrtProjectDir)' == ''">$(MSBuildProjectDirectory)</NrtProjectDir>
Expand All @@ -37,7 +38,9 @@
RemoveTagV="$(NrtRemoveTagV)"
ResolveCopyright="$(NrtResolveCopyright)"
Copyright="$(Copyright)"
ShowRevision="$(NrtShowRevision)">
ShowRevision="$(NrtShowRevision)"
ConfigurationName="$(ConfigurationName)"
ErrorOnModifiedRepoPattern="$(NrtErrorOnModifiedRepoPattern)">
<Output TaskParameter="Version" PropertyName="Version"/>
<Output TaskParameter="InformationalVersion" PropertyName="InformationalVersion"/>
<Output TaskParameter="Copyright" PropertyName="Copyright"/>
Expand Down Expand Up @@ -72,7 +75,10 @@
ResolveInformationalAttribute="$(NrtResolveInformationalAttribute)"
RevisionNumberOnly="$(NrtRevisionNumberOnly)"
ResolveCopyright="$(NrtResolveCopyright)"
ShowRevision="$(NrtShowRevision)">
ShowRevision="$(NrtShowRevision)"
ResolveMetadata="$(NrtResolveMetadata)"
ConfigurationName="$(ConfigurationName)"
ErrorOnModifiedRepoPattern="$(NrtErrorOnModifiedRepoPattern)">
<Output TaskParameter="SourceAssemblyInfo" PropertyName="NrtSourceAssemblyInfo"/>
<Output TaskParameter="PatchedAssemblyInfo" PropertyName="NrtPatchedAssemblyInfo"/>
</PatchAssemblyInfo>
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Example:
<NrtRequiredVcs>git</NrtRequiredVcs>
<NrtShowRevision>true</NrtShowRevision>
<NrtProjectDirectory>$(MSBuildProjectDirectory)</NrtProjectDirectory>
<NrtResolveMetadata>true</NrtResolveMetadata>
<NrtErrorOnModifiedRepoPattern>.*Release.*</NrtErrorOnModifiedRepoPattern>
</PropertyGroup>

The following MSBuild properties are supported:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -183,6 +193,12 @@ The following data field placeholders are supported:

**`{copyright:<first>-}`**: Abbreviation for the copyright year range, starting at `<first>`. The following dash is optional but recommended for clearer understanding.

**`{bconf}`**: Build configuration.

**`{BCONF}`**: Build configuration, in upper case.

**`{bconf:<sep>:<ref>}`, `{BCONF:<sep>:<ref>}`**: Build configuration, if not matching case-insensitive RegEx `<ref>` pattern, separated by `<sep>`, 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:
Expand Down