tools/net-changelog-gen-mgmt/Azure.SDK.Management.ChangelogGen/Utilities/Helper.cs (197 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using LibGit2Sharp;
using Markdig.Syntax;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Azure.SDK.ChangelogGen.Utilities
{
internal static class Helper
{
#region release
public static string GetLastReleaseVersionFromGitBranch(this Branch branch, string fileKey, bool includePreview, out string releaseDate)
{
string content = branch.GetFileContent(fileKey);
return GetLastReleaseVersion(content, includePreview, out releaseDate);
}
public static string GetLastReleaseVersionFromGitTree(this Tree tree, string fileKey, bool includePreview, out string releaseDate)
{
string content = tree.GetFileContent(fileKey);
return GetLastReleaseVersion(content, includePreview, out releaseDate);
}
public static string GetLastReleaseVersionFromFile(string path, bool includePreview, out string releaseDate)
{
string content = File.ReadAllText(path);
return GetLastReleaseVersion(content, includePreview, out releaseDate);
}
private static string GetLastReleaseVersion(string changelogContent, bool includePreview, out string releaseDate)
{
Regex ver = new Regex(@"^##\s+(?<ver>[\w\.\-]+?)\s+\((?<date>\d{4}-\d{2}-\d{2})\)\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase);
var matches = ver.Matches(changelogContent).Where(m => includePreview || !IsPreviewRelease(m.Groups["ver"].Value)).ToList();
if (matches.Count == 0)
{
releaseDate = "";
return "";
}
else
{
var firstMatch = matches[0];
releaseDate = firstMatch.Groups["date"].Value;
return firstMatch.Groups["ver"].Value;
}
}
public static bool IsPreviewRelease(string version)
{
return Regex.IsMatch(version, @"beta|alpha|preview", RegexOptions.IgnoreCase);
}
#endregion
#region git
public static string GetFileContent(this TreeEntry te)
{
Debug.Assert(te.TargetType == TreeEntryTargetType.Blob);
var blob = (LibGit2Sharp.Blob)te.Target;
var contentStream = blob.GetContentStream();
using (var tr = new StreamReader(contentStream, Encoding.UTF8))
{
return tr.ReadToEnd();
}
}
public static Branch GetBranch(this Repository repo, string? branchFriendlyName = null)
{
var b = string.IsNullOrEmpty(branchFriendlyName) ? repo.Branches.FirstOrDefault(b => b.IsCurrentRepositoryHead) : repo.Branches.FirstOrDefault(b => b.FriendlyName == branchFriendlyName);
if (b == null)
{
throw new InvalidOperationException("Can't find branch with FriendlyName " + (branchFriendlyName ?? "CurrentBranch"));
}
return b;
}
public static Tree GetTreeByTag(this Repository repo, string tagFriendlyName)
{
var tag = repo.Tags.FirstOrDefault(t => t.FriendlyName == tagFriendlyName) ??
throw new InvalidOperationException($"Can't find Tag with FriendlyName {tagFriendlyName}. Please make sure the Tag exists (i.e. not released yet?) and has been refreshed properly locally (i.e. by 'git fetch upstream main --tags')");
var commit = repo.Lookup<Commit>(tag.Target.Id) ??
throw new InvalidOperationException("Can't find the commit for Tag " + tagFriendlyName);
return commit.Tree;
}
public static string GetFileContent(this Branch branch, string fileKey)
{
return branch.Tip.Tree.GetFileContent(fileKey);
}
public static string GetFileContent(this Tree tree, string fileKey)
{
string[] segs = fileKey.Split('\\', '/');
if (segs.Length == 0)
throw new ArgumentNullException(nameof(fileKey));
Tree cur = tree;
for (int i = 0; i < segs.Length - 1; i++)
{
TreeEntry? curSeg = cur.FirstOrDefault(c => c.Name.ToLower() == segs[i].ToLower()) ??
throw new InvalidOperationException($"Can't find file '{fileKey}' in Git at '{segs[i]}'. Git tree id = '{tree.Id}'");
Debug.Assert(curSeg.Target.GetType() == typeof(Tree));
cur = (Tree)curSeg.Target;
}
var te = cur.FirstOrDefault(c => c.Name.ToLower() == segs[^1].ToLower()) ??
throw new InvalidOperationException($"Cannot find file '{fileKey}' in Git at '{segs[^1]}'. Git tree id = '{tree.Id}'");
return te.GetFileContent();
}
#endregion
#region api
public static bool IsObsoleted(this PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttribute<ObsoleteAttribute>() != null;
}
public static bool IsObsoleted(this MethodInfo methodInfo)
{
return methodInfo.GetCustomAttribute<ObsoleteAttribute>() != null;
}
public static bool IsObsoleted(this Type type)
{
return type.GetCustomAttribute<ObsoleteAttribute>() != null;
}
public static bool IsObsoleted(this ConstructorInfo constructorInfo)
{
return constructorInfo.GetCustomAttribute<ObsoleteAttribute>() != null;
}
public static MethodInfo[] GetMethods(this Type type, BindingFlags flags, bool includePropertyMethod)
{
if (includePropertyMethod)
return type.GetMethods(flags);
else
return type.GetMethods(flags).Where(m => !m.Name.StartsWith("get_") && !m.Name.StartsWith("set_")).ToArray();
}
public static string ToFriendlyString(this PropertyInfo pi)
{
if (pi == null)
throw new ArgumentNullException(nameof(pi));
var indexParameters = pi.GetIndexParameters();
if (indexParameters.Length > 0)
{
return $"{pi.PropertyType.ToFriendlyString()} {pi.DeclaringType!.ToFriendlyString()}[{string.Join(", ", indexParameters.Select(p => p.ParameterType.ToFriendlyString()))}]";
}
else
{
return $"{pi.PropertyType.ToFriendlyString()} {pi.Name}";
}
}
public static string ToFriendlyString(this MethodInfo mi)
{
if (mi == null)
throw new ArgumentNullException(nameof(mi));
var genericArguments = mi.GetGenericArguments();
var genericPart = genericArguments.Length == 0 ? "" : $"<{string.Join(", ", genericArguments.Select(g => g.Name))}>";
var paramList = mi.GetParameters();
var returnType = mi.ReturnType;
string paramString = string.Join(", ", paramList.Select(p => $"{p.ParameterType.ToFriendlyString()} {p.Name}{(p.HasDefaultValue && p.DefaultValue != null ? $" = {p.DefaultValue}" : "")}"));
return $"{returnType.ToFriendlyString()} {mi.Name}{genericPart}({paramString})";
}
public static string ToFriendlyString(this Type type, bool fullName = false)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
var gtp = type.GetGenericArguments();
var genericPart = gtp.Length == 0 ? "" : $"<{string.Join(", ", gtp.Select(t => t.ToFriendlyString(fullName)))}>";
string name = (fullName && type.FullName != null) ? type.FullName : type.Name;
int index = name.IndexOf('`');
if (index >= 0)
name = name[..index];
return $"{name}{genericPart}";
}
public static string ToFriendlyString(this ConstructorInfo ci)
{
if (ci == null)
throw new ArgumentNullException(nameof(ci));
var paramList = ci.GetParameters();
return $"{ci.Name}({string.Join(", ", paramList.Select(p => $"{p.ParameterType.ToFriendlyString()} {p.Name}"))})";
}
public static string GetKey(this Type type)
{
return type.FullName!;
}
public static string GetKey(this MethodInfo mi)
{
// Use Friendly string we parsed instead of default ToString() because the default ToString() won't consider the parameter default value
return mi.ToFriendlyString();
}
public static string GetKey(this PropertyInfo pi)
{
return pi.ToString()!;
}
public static string GetKey(this ConstructorInfo pi)
{
return pi.ToString()!;
}
#endregion
#region markdown
public static Dictionary<string, object> LoadYaml(this MarkdownDocument md)
{
var lines = md.Where(b =>
{
FencedCodeBlock? fcb = b as FencedCodeBlock;
if (fcb == null)
return false;
return (fcb.Info == "yaml" && string.IsNullOrEmpty(fcb.Arguments));
}).SelectMany(b => ((FencedCodeBlock)b).Lines.Lines).ToList();
// TODO: add support to include yaml from conditional block in markdown when needed
var allYaml = string.Join("\n", lines);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(HyphenatedNamingConvention.Instance)
.Build();
return deserializer.Deserialize<Dictionary<string, object>>(allYaml);
}
#endregion
}
}